Merge branch 'develop' into chore-global-config-mixin
This commit is contained in:
@@ -1,13 +1,12 @@
|
||||
<script>
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import { useAI } from 'dashboard/composables/useAI';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import AILoader from './AILoader.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AILoader,
|
||||
},
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
aiOption: {
|
||||
type: String,
|
||||
@@ -15,12 +14,9 @@ export default {
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
const { draftMessage, processEvent, recordAnalytics } = useAI();
|
||||
return {
|
||||
draftMessage,
|
||||
processEvent,
|
||||
recordAnalytics,
|
||||
};
|
||||
return { draftMessage, processEvent, recordAnalytics, formatMessage };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import BubbleActions from './bubble/Actions.vue';
|
||||
import BubbleContact from './bubble/Contact.vue';
|
||||
import BubbleFile from './bubble/File.vue';
|
||||
@@ -39,7 +39,6 @@ export default {
|
||||
InstagramStoryReply,
|
||||
Spinner,
|
||||
},
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
@@ -74,6 +73,12 @@ export default {
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
return {
|
||||
formatMessage,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showContextMenu: false,
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
<script>
|
||||
import { MESSAGE_TYPE } from 'widget/helpers/constants';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import { ATTACHMENT_ICONS } from 'shared/constants/messages';
|
||||
|
||||
export default {
|
||||
name: 'MessagePreview',
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
message: {
|
||||
type: Object,
|
||||
@@ -20,6 +19,12 @@ export default {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { getPlainText } = useMessageFormatter();
|
||||
return {
|
||||
getPlainText,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
messageByAgent() {
|
||||
const { message_type: messageType } = this.message;
|
||||
|
||||
@@ -17,7 +17,6 @@ import Banner from 'dashboard/components/ui/Banner.vue';
|
||||
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
|
||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||
import WootAudioRecorder from 'dashboard/components/widgets/WootWriter/AudioRecorder.vue';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { AUDIO_FORMATS } from 'shared/constants/messages';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import {
|
||||
@@ -61,12 +60,7 @@ export default {
|
||||
MessageSignatureMissingAlert,
|
||||
ArticleSearchPopover,
|
||||
},
|
||||
mixins: [
|
||||
inboxMixin,
|
||||
messageFormatterMixin,
|
||||
fileUploadMixin,
|
||||
keyboardEventListenerMixins,
|
||||
],
|
||||
mixins: [inboxMixin, fileUploadMixin, keyboardEventListenerMixins],
|
||||
props: {
|
||||
popoutReplyBox: {
|
||||
type: Boolean,
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
export const summary = {
|
||||
avg_first_response_time: '198.6666666666667',
|
||||
avg_resolution_time: '208.3333333333333',
|
||||
conversations_count: 5000,
|
||||
incoming_messages_count: 5,
|
||||
outgoing_messages_count: 3,
|
||||
previous: {
|
||||
avg_first_response_time: '89.0',
|
||||
avg_resolution_time: '145.0',
|
||||
conversations_count: 4,
|
||||
incoming_messages_count: 5,
|
||||
outgoing_messages_count: 4,
|
||||
resolutions_count: 0,
|
||||
},
|
||||
resolutions_count: 3,
|
||||
};
|
||||
|
||||
export const botSummary = {
|
||||
bot_resolutions_count: 10,
|
||||
bot_handoffs_count: 20,
|
||||
previous: {
|
||||
bot_resolutions_count: 8,
|
||||
bot_handoffs_count: 5,
|
||||
},
|
||||
};
|
||||
|
||||
export const report = {
|
||||
data: [
|
||||
{ value: '0.00', timestamp: 1647541800, count: 0 },
|
||||
{ value: '0.00', timestamp: 1647628200, count: 0 },
|
||||
{ value: '0.00', timestamp: 1647714600, count: 0 },
|
||||
{ value: '0.00', timestamp: 1647801000, count: 0 },
|
||||
{ value: '0.01', timestamp: 1647887400, count: 4 },
|
||||
{ value: '0.00', timestamp: 1647973800, count: 0 },
|
||||
{ value: '0.00', timestamp: 1648060200, count: 0 },
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import { ref } from 'vue';
|
||||
import { useReportMetrics } from '../useReportMetrics';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { summary, botSummary } from './fixtures/reportFixtures';
|
||||
|
||||
vi.mock('dashboard/composables/store');
|
||||
vi.mock('@chatwoot/utils', () => ({
|
||||
formatTime: vi.fn(time => `formatted_${time}`),
|
||||
}));
|
||||
|
||||
describe('useReportMetrics', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useMapGetter.mockReturnValue(ref(summary));
|
||||
});
|
||||
|
||||
it('calculates trend correctly', () => {
|
||||
const { calculateTrend } = useReportMetrics();
|
||||
|
||||
expect(calculateTrend('conversations_count')).toBe(124900);
|
||||
expect(calculateTrend('incoming_messages_count')).toBe(0);
|
||||
expect(calculateTrend('avg_first_response_time')).toBe(123);
|
||||
});
|
||||
|
||||
it('returns 0 for trend when previous value is not available', () => {
|
||||
const { calculateTrend } = useReportMetrics();
|
||||
|
||||
expect(calculateTrend('non_existent_key')).toBe(0);
|
||||
});
|
||||
|
||||
it('identifies average metric types correctly', () => {
|
||||
const { isAverageMetricType } = useReportMetrics();
|
||||
|
||||
expect(isAverageMetricType('avg_first_response_time')).toBe(true);
|
||||
expect(isAverageMetricType('avg_resolution_time')).toBe(true);
|
||||
expect(isAverageMetricType('reply_time')).toBe(true);
|
||||
expect(isAverageMetricType('conversations_count')).toBe(false);
|
||||
});
|
||||
|
||||
it('displays metrics correctly for account', () => {
|
||||
const { displayMetric } = useReportMetrics();
|
||||
|
||||
expect(displayMetric('conversations_count')).toBe('5,000');
|
||||
expect(displayMetric('incoming_messages_count')).toBe('5');
|
||||
});
|
||||
|
||||
it('displays the metric for bot', () => {
|
||||
const customKey = 'getBotSummary';
|
||||
useMapGetter.mockReturnValue(ref(botSummary));
|
||||
const { displayMetric } = useReportMetrics(customKey);
|
||||
|
||||
expect(displayMetric('bot_resolutions_count')).toBe('10');
|
||||
expect(displayMetric('bot_handoffs_count')).toBe('20');
|
||||
});
|
||||
|
||||
it('handles non-existent metrics', () => {
|
||||
const { displayMetric } = useReportMetrics();
|
||||
|
||||
expect(displayMetric('non_existent_key')).toBe('0');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { formatTime } from '@chatwoot/utils';
|
||||
|
||||
/**
|
||||
* A composable function for report metrics calculations and display.
|
||||
*
|
||||
* @param {string} [accountSummaryKey='getAccountSummary'] - The key for accessing account summary data.
|
||||
* @returns {Object} An object containing utility functions for report metrics.
|
||||
*/
|
||||
export function useReportMetrics(accountSummaryKey = 'getAccountSummary') {
|
||||
const accountSummary = useMapGetter(accountSummaryKey);
|
||||
|
||||
/**
|
||||
* Calculates the trend percentage for a given metric.
|
||||
*
|
||||
* @param {string} key - The key of the metric to calculate trend for.
|
||||
* @returns {number} The calculated trend percentage, rounded to the nearest integer.
|
||||
*/
|
||||
const calculateTrend = key => {
|
||||
if (!accountSummary.value.previous[key]) return 0;
|
||||
const diff = accountSummary.value[key] - accountSummary.value.previous[key];
|
||||
return Math.round((diff / accountSummary.value.previous[key]) * 100);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a given metric key represents an average metric type.
|
||||
*
|
||||
* @param {string} key - The key of the metric to check.
|
||||
* @returns {boolean} True if the metric is an average type, false otherwise.
|
||||
*/
|
||||
const isAverageMetricType = key => {
|
||||
return [
|
||||
'avg_first_response_time',
|
||||
'avg_resolution_time',
|
||||
'reply_time',
|
||||
].includes(key);
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats and displays a metric value based on its type.
|
||||
*
|
||||
* @param {string} key - The key of the metric to display.
|
||||
* @returns {string} The formatted metric value as a string.
|
||||
*/
|
||||
const displayMetric = key => {
|
||||
if (isAverageMetricType(key)) {
|
||||
return formatTime(accountSummary.value[key]);
|
||||
}
|
||||
return Number(accountSummary.value[key] || '').toLocaleString();
|
||||
};
|
||||
|
||||
return {
|
||||
calculateTrend,
|
||||
isAverageMetricType,
|
||||
displayMetric,
|
||||
};
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import { formatTime } from '@chatwoot/utils';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
accountSummaryKey: {
|
||||
type: String,
|
||||
default: 'getAccountSummary',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountReport: 'getAccountReports',
|
||||
}),
|
||||
accountSummary() {
|
||||
return this.$store.getters[this.accountSummaryKey];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
calculateTrend(key) {
|
||||
if (!this.accountSummary.previous[key]) return 0;
|
||||
const diff = this.accountSummary[key] - this.accountSummary.previous[key];
|
||||
return Math.round((diff / this.accountSummary.previous[key]) * 100);
|
||||
},
|
||||
displayMetric(key) {
|
||||
if (this.isAverageMetricType(key)) {
|
||||
return formatTime(this.accountSummary[key]);
|
||||
}
|
||||
return Number(this.accountSummary[key] || '').toLocaleString();
|
||||
},
|
||||
displayInfoText(key) {
|
||||
if (this.metrics[this.currentSelection].KEY !== key) {
|
||||
return '';
|
||||
}
|
||||
if (this.isAverageMetricType(key)) {
|
||||
const total = this.accountReport.data
|
||||
.map(item => item.count)
|
||||
.reduce((prev, curr) => prev + curr, 0);
|
||||
return `${this.metrics[this.currentSelection].INFO_TEXT} ${total}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
isAverageMetricType(key) {
|
||||
return [
|
||||
'avg_first_response_time',
|
||||
'avg_resolution_time',
|
||||
'reply_time',
|
||||
].includes(key);
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,136 +0,0 @@
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import reportMixin from '../reportMixin';
|
||||
import reportFixtures from './reportMixinFixtures';
|
||||
import Vuex from 'vuex';
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
describe('reportMixin', () => {
|
||||
let getters;
|
||||
let store;
|
||||
beforeEach(() => {
|
||||
getters = {
|
||||
getAccountSummary: () => reportFixtures.summary,
|
||||
getBotSummary: () => reportFixtures.botSummary,
|
||||
getAccountReports: () => reportFixtures.report,
|
||||
};
|
||||
store = new Vuex.Store({ getters });
|
||||
});
|
||||
|
||||
it('display the metric for account', async () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [reportMixin],
|
||||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
await wrapper.setProps({
|
||||
accountSummaryKey: 'getAccountSummary',
|
||||
});
|
||||
expect(wrapper.vm.displayMetric('conversations_count')).toEqual('5,000');
|
||||
expect(wrapper.vm.displayMetric('avg_first_response_time')).toEqual(
|
||||
'3 Min 18 Sec'
|
||||
);
|
||||
});
|
||||
|
||||
it('display the metric for bot', async () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [reportMixin],
|
||||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
await wrapper.setProps({
|
||||
accountSummaryKey: 'getBotSummary',
|
||||
});
|
||||
expect(wrapper.vm.displayMetric('bot_resolutions_count')).toEqual('10');
|
||||
expect(wrapper.vm.displayMetric('bot_handoffs_count')).toEqual('20');
|
||||
});
|
||||
|
||||
it('display the metric', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [reportMixin],
|
||||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
expect(wrapper.vm.displayMetric('conversations_count')).toEqual('5,000');
|
||||
expect(wrapper.vm.displayMetric('avg_first_response_time')).toEqual(
|
||||
'3 Min 18 Sec'
|
||||
);
|
||||
});
|
||||
|
||||
it('calculate the trend', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [reportMixin],
|
||||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
expect(wrapper.vm.calculateTrend('conversations_count')).toEqual(124900);
|
||||
expect(wrapper.vm.calculateTrend('resolutions_count')).toEqual(0);
|
||||
});
|
||||
|
||||
it('display info text', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [reportMixin],
|
||||
data() {
|
||||
return {
|
||||
currentSelection: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
metrics() {
|
||||
return [
|
||||
{
|
||||
DESC: '( Avg )',
|
||||
INFO_TEXT: 'Total number of conversations used for computation:',
|
||||
KEY: 'avg_first_response_time',
|
||||
NAME: 'First Response Time',
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
expect(wrapper.vm.displayInfoText('avg_first_response_time')).toEqual(
|
||||
'Total number of conversations used for computation: 4'
|
||||
);
|
||||
});
|
||||
|
||||
it('do not display info text', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [reportMixin],
|
||||
data() {
|
||||
return {
|
||||
currentSelection: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
metrics() {
|
||||
return [
|
||||
{
|
||||
DESC: '( Total )',
|
||||
INFO_TEXT: '',
|
||||
KEY: 'conversation_count',
|
||||
NAME: 'Conversations',
|
||||
},
|
||||
{
|
||||
DESC: '( Avg )',
|
||||
INFO_TEXT: 'Total number of conversations used for computation:',
|
||||
KEY: 'avg_first_response_time',
|
||||
NAME: 'First Response Time',
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
expect(wrapper.vm.displayInfoText('conversation_count')).toEqual('');
|
||||
expect(wrapper.vm.displayInfoText('incoming_messages_count')).toEqual('');
|
||||
});
|
||||
});
|
||||
@@ -1,37 +0,0 @@
|
||||
export default {
|
||||
summary: {
|
||||
avg_first_response_time: '198.6666666666667',
|
||||
avg_resolution_time: '208.3333333333333',
|
||||
conversations_count: 5000,
|
||||
incoming_messages_count: 5,
|
||||
outgoing_messages_count: 3,
|
||||
previous: {
|
||||
avg_first_response_time: '89.0',
|
||||
avg_resolution_time: '145.0',
|
||||
conversations_count: 4,
|
||||
incoming_messages_count: 5,
|
||||
outgoing_messages_count: 4,
|
||||
resolutions_count: 0,
|
||||
},
|
||||
resolutions_count: 3,
|
||||
},
|
||||
botSummary: {
|
||||
bot_resolutions_count: 10,
|
||||
bot_handoffs_count: 20,
|
||||
previous: {
|
||||
bot_resolutions_count: 8,
|
||||
bot_handoffs_count: 5,
|
||||
},
|
||||
},
|
||||
report: {
|
||||
data: [
|
||||
{ value: '0.00', timestamp: 1647541800, count: 0 },
|
||||
{ value: '0.00', timestamp: 1647628200, count: 0 },
|
||||
{ value: '0.00', timestamp: 1647714600, count: 0 },
|
||||
{ value: '0.00', timestamp: 1647801000, count: 0 },
|
||||
{ value: '0.01', timestamp: 1647887400, count: 4 },
|
||||
{ value: '0.00', timestamp: 1647973800, count: 0 },
|
||||
{ value: '0.00', timestamp: 1648060200, count: 0 },
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { mapGetters } from 'vuex';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import AddCannedModal from 'dashboard/routes/dashboard/settings/canned/AddCanned.vue';
|
||||
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
||||
import { conversationUrl, frontendURL } from '../../../helper/URLHelper';
|
||||
@@ -18,7 +18,6 @@ export default {
|
||||
TranslateModal,
|
||||
MenuItem,
|
||||
},
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
message: {
|
||||
type: Object,
|
||||
@@ -37,6 +36,12 @@ export default {
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { getPlainText } = useMessageFormatter();
|
||||
return {
|
||||
getPlainText,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isCannedResponseModalOpen: false,
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
<script>
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
},
|
||||
|
||||
mixins: [messageFormatterMixin],
|
||||
|
||||
props: {
|
||||
id: {
|
||||
type: Number,
|
||||
@@ -28,6 +25,12 @@ export default {
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
return {
|
||||
formatMessage,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showDeleteModal: false,
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<script>
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import ReadMore from './ReadMore.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ReadMore,
|
||||
},
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
author: {
|
||||
type: String,
|
||||
@@ -21,6 +20,13 @@ export default {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { formatMessage, highlightContent } = useMessageFormatter();
|
||||
return {
|
||||
formatMessage,
|
||||
highlightContent,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isOverflowing: false,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import SwitchLayout from './SwitchLayout.vue';
|
||||
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||
export default {
|
||||
@@ -14,7 +13,6 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
isOnExpandedLayout: {
|
||||
type: Boolean,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
@@ -7,12 +7,12 @@ import BillingItem from './components/BillingItem.vue';
|
||||
|
||||
export default {
|
||||
components: { BillingItem },
|
||||
mixins: [messageFormatterMixin],
|
||||
setup() {
|
||||
const { accountId } = useAccount();
|
||||
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
return {
|
||||
accountId,
|
||||
formatMessage,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import UserAvatarWithName from 'dashboard/components/widgets/UserAvatarWithName.vue';
|
||||
import InboxName from 'dashboard/components/widgets/InboxName.vue';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import { messageStamp } from 'shared/helpers/timeHelper';
|
||||
|
||||
export default {
|
||||
@@ -9,7 +9,6 @@ export default {
|
||||
UserAvatarWithName,
|
||||
InboxName,
|
||||
},
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
campaign: {
|
||||
type: Object,
|
||||
@@ -20,7 +19,12 @@ export default {
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
setup() {
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
return {
|
||||
formatMessage,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
campaignStatus() {
|
||||
if (this.isOngoingType) {
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import Integration from './Integration.vue';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import SelectChannelWarning from './Slack/SelectChannelWarning.vue';
|
||||
import SlackIntegrationHelpText from './Slack/SlackIntegrationHelpText.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
@@ -13,7 +12,7 @@ export default {
|
||||
SelectChannelWarning,
|
||||
SlackIntegrationHelpText,
|
||||
},
|
||||
mixins: [globalConfigMixin, messageFormatterMixin],
|
||||
mixins: [globalConfigMixin],
|
||||
props: {
|
||||
code: { type: String, default: '' },
|
||||
},
|
||||
|
||||
+7
-2
@@ -2,16 +2,21 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useInstallationName } from 'shared/helpers/installationNameHelper';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
|
||||
export default {
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
hasConnectedAChannel: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
return {
|
||||
formatMessage,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return { selectedChannelId: '', availableChannels: [] };
|
||||
},
|
||||
|
||||
+7
-2
@@ -1,13 +1,18 @@
|
||||
<script>
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
export default {
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
selectedChannelName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
return {
|
||||
formatMessage,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useAlert } from 'dashboard/composables';
|
||||
import BotMetrics from './components/BotMetrics.vue';
|
||||
import ReportFilterSelector from './components/FilterSelector.vue';
|
||||
import { GROUP_BY_FILTER } from './constants';
|
||||
import reportMixin from 'dashboard/mixins/reportMixin';
|
||||
import ReportContainer from './ReportContainer.vue';
|
||||
import { REPORTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||
|
||||
@@ -14,7 +13,6 @@ export default {
|
||||
ReportFilterSelector,
|
||||
ReportContainer,
|
||||
},
|
||||
mixins: [reportMixin],
|
||||
data() {
|
||||
return {
|
||||
from: 0,
|
||||
|
||||
@@ -4,7 +4,6 @@ import fromUnixTime from 'date-fns/fromUnixTime';
|
||||
import format from 'date-fns/format';
|
||||
import ReportFilterSelector from './components/FilterSelector.vue';
|
||||
import { GROUP_BY_FILTER } from './constants';
|
||||
import reportMixin from 'dashboard/mixins/reportMixin';
|
||||
import { REPORTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||
import ReportContainer from './ReportContainer.vue';
|
||||
|
||||
@@ -24,7 +23,6 @@ export default {
|
||||
ReportFilterSelector,
|
||||
ReportContainer,
|
||||
},
|
||||
mixins: [reportMixin],
|
||||
data() {
|
||||
return {
|
||||
from: 0,
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useReportMetrics } from 'dashboard/composables/useReportMetrics';
|
||||
import { GROUP_BY_FILTER, METRIC_CHART } from './constants';
|
||||
import fromUnixTime from 'date-fns/fromUnixTime';
|
||||
import format from 'date-fns/format';
|
||||
import { formatTime } from '@chatwoot/utils';
|
||||
import reportMixin from 'dashboard/mixins/reportMixin';
|
||||
import ChartStats from './components/ChartElements/ChartStats.vue';
|
||||
|
||||
export default {
|
||||
components: { ChartStats },
|
||||
mixins: [reportMixin],
|
||||
props: {
|
||||
groupBy: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
accountSummaryKey: {
|
||||
type: String,
|
||||
default: 'getAccountSummary',
|
||||
},
|
||||
reportKeys: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
@@ -27,7 +31,16 @@ export default {
|
||||
}),
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { calculateTrend, isAverageMetricType } = useReportMetrics(
|
||||
props.accountSummaryKey
|
||||
);
|
||||
return { calculateTrend, isAverageMetricType };
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountReport: 'getAccountReports',
|
||||
}),
|
||||
metrics() {
|
||||
const reportKeys = Object.keys(this.reportKeys);
|
||||
const infoText = {
|
||||
@@ -121,12 +134,12 @@ export default {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 bg-white dark:bg-slate-800 p-2 border border-slate-100 dark:border-slate-700 rounded-md"
|
||||
class="grid grid-cols-1 p-2 bg-white border rounded-md md:grid-cols-2 lg:grid-cols-3 dark:bg-slate-800 border-slate-100 dark:border-slate-700"
|
||||
>
|
||||
<div
|
||||
v-for="metric in metrics"
|
||||
:key="metric.KEY"
|
||||
class="p-4 rounded-md mb-3"
|
||||
class="p-4 mb-3 rounded-md"
|
||||
>
|
||||
<ChartStats :metric="metric" :account-summary-key="accountSummaryKey" />
|
||||
<div class="mt-4 h-72">
|
||||
@@ -135,12 +148,12 @@ export default {
|
||||
class="text-xs"
|
||||
:message="$t('REPORT.LOADING_CHART')"
|
||||
/>
|
||||
<div v-else class="h-72 flex items-center justify-center">
|
||||
<div v-else class="flex items-center justify-center h-72">
|
||||
<woot-bar
|
||||
v-if="accountReport.data[metric.KEY].length"
|
||||
:collection="getCollection(metric)"
|
||||
:chart-options="getChartOptions(metric)"
|
||||
class="h-72 w-full"
|
||||
class="w-full h-72"
|
||||
/>
|
||||
<span v-else class="text-sm text-slate-600">
|
||||
{{ $t('REPORT.NO_ENOUGH_DATA') }}
|
||||
|
||||
+26
-21
@@ -1,25 +1,30 @@
|
||||
<script>
|
||||
import reportMixin from 'dashboard/mixins/reportMixin';
|
||||
export default {
|
||||
mixins: [reportMixin],
|
||||
props: {
|
||||
metric: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
<script setup>
|
||||
import { useReportMetrics } from 'dashboard/composables/useReportMetrics';
|
||||
|
||||
const props = defineProps({
|
||||
metric: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
methods: {
|
||||
trendColor(value, key) {
|
||||
if (this.isAverageMetricType(key)) {
|
||||
return value > 0
|
||||
? 'border-red-500 text-red-500'
|
||||
: 'border-green-500 text-green-500';
|
||||
}
|
||||
return value < 0
|
||||
? 'border-red-500 text-red-500'
|
||||
: 'border-green-500 text-green-500';
|
||||
},
|
||||
accountSummaryKey: {
|
||||
type: String,
|
||||
default: 'getAccountSummary',
|
||||
},
|
||||
});
|
||||
|
||||
const { calculateTrend, displayMetric, isAverageMetricType } = useReportMetrics(
|
||||
props.accountSummaryKey
|
||||
);
|
||||
|
||||
const trendColor = (value, key) => {
|
||||
if (isAverageMetricType(key)) {
|
||||
return value > 0
|
||||
? 'border-red-500 text-red-500'
|
||||
: 'border-green-500 text-green-500';
|
||||
}
|
||||
return value < 0
|
||||
? 'border-red-500 text-red-500'
|
||||
: 'border-green-500 text-green-500';
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -29,7 +34,7 @@ export default {
|
||||
{{ metric.NAME }}
|
||||
</span>
|
||||
<div class="flex items-end">
|
||||
<div class="font-medium text-xl">
|
||||
<div class="text-xl font-medium">
|
||||
{{ displayMetric(metric.KEY) }}
|
||||
</div>
|
||||
<div v-if="metric.trend" class="text-xs ml-4 flex items-center mb-0.5">
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useAlert } from 'dashboard/composables';
|
||||
import ReportFilters from './ReportFilters.vue';
|
||||
import ReportContainer from '../ReportContainer.vue';
|
||||
import { GROUP_BY_FILTER } from '../constants';
|
||||
import reportMixin from '../../../../../mixins/reportMixin';
|
||||
import { generateFileName } from '../../../../../helper/downloadHelper';
|
||||
import { REPORTS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
|
||||
|
||||
@@ -22,7 +21,6 @@ export default {
|
||||
ReportFilters,
|
||||
ReportContainer,
|
||||
},
|
||||
mixins: [reportMixin],
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<script>
|
||||
import { ref, computed, nextTick } from 'vue';
|
||||
import { useKeyboardNavigableList } from 'dashboard/composables/useKeyboardNavigableList';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
|
||||
export default {
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
@@ -30,7 +29,7 @@ export default {
|
||||
setup(props) {
|
||||
const selectedIndex = ref(-1);
|
||||
const portalSearchSuggestionsRef = ref(null);
|
||||
|
||||
const { highlightContent } = useMessageFormatter();
|
||||
const adjustScroll = () => {
|
||||
nextTick(() => {
|
||||
portalSearchSuggestionsRef.value.scrollTop = 102 * selectedIndex.value;
|
||||
@@ -53,6 +52,7 @@ export default {
|
||||
selectedIndex,
|
||||
portalSearchSuggestionsRef,
|
||||
isSearchItemActive,
|
||||
highlightContent,
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<script>
|
||||
import ChatOption from 'shared/components/ChatOption.vue';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ChatOption,
|
||||
},
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
@@ -25,6 +24,12 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
return {
|
||||
formatMessage,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
isSelected(option) {
|
||||
return this.selected === option.id;
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { useMessageFormatter } from '../useMessageFormatter';
|
||||
|
||||
describe('useMessageFormatter', () => {
|
||||
let messageFormatter;
|
||||
|
||||
beforeEach(() => {
|
||||
messageFormatter = useMessageFormatter();
|
||||
});
|
||||
|
||||
describe('formatMessage', () => {
|
||||
it('should format a regular message correctly', () => {
|
||||
const message = 'This is a [test](https://example.com) message';
|
||||
const result = messageFormatter.formatMessage(message, false, false);
|
||||
expect(result).toContain('<a href="https://example.com"');
|
||||
expect(result).toContain('class="link"');
|
||||
});
|
||||
|
||||
it('should format a tweet correctly', () => {
|
||||
const message = '@user #hashtag';
|
||||
const result = messageFormatter.formatMessage(message, true, false);
|
||||
expect(result).toContain('<a href="http://twitter.com/user"');
|
||||
expect(result).toContain('<a href="https://twitter.com/hashtag/hashtag"');
|
||||
});
|
||||
|
||||
it('should not format mentions and hashtags for private notes', () => {
|
||||
const message = '@user #hashtag';
|
||||
const result = messageFormatter.formatMessage(message, false, true);
|
||||
expect(result).not.toContain('<a href="http://twitter.com/user"');
|
||||
expect(result).not.toContain(
|
||||
'<a href="https://twitter.com/hashtag/hashtag"'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('truncateMessage', () => {
|
||||
it('should not truncate short messages', () => {
|
||||
const message = 'Short message';
|
||||
const result = messageFormatter.truncateMessage(message);
|
||||
expect(result).toBe(message);
|
||||
});
|
||||
|
||||
it('should truncate long messages', () => {
|
||||
const message = 'A'.repeat(150);
|
||||
const result = messageFormatter.truncateMessage(message);
|
||||
expect(result.length).toBe(100);
|
||||
expect(result.endsWith('...')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('highlightContent', () => {
|
||||
it('should highlight search term in content', () => {
|
||||
const content = 'This is a test message';
|
||||
const searchTerm = 'test';
|
||||
const highlightClass = 'highlight';
|
||||
const result = messageFormatter.highlightContent(
|
||||
content,
|
||||
searchTerm,
|
||||
highlightClass
|
||||
);
|
||||
expect(result.trim()).toBe(
|
||||
'This is a <span class="highlight">test</span> message'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle special characters in search term', () => {
|
||||
const content = 'This (message) contains [special] characters';
|
||||
const searchTerm = '(message)';
|
||||
const highlightClass = 'highlight';
|
||||
const result = messageFormatter.highlightContent(
|
||||
content,
|
||||
searchTerm,
|
||||
highlightClass
|
||||
);
|
||||
expect(result.trim()).toBe(
|
||||
'This <span class="highlight">(message)</span> contains [special] characters'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
import MessageFormatter from '../helpers/MessageFormatter';
|
||||
|
||||
/**
|
||||
* A composable providing utility functions for message formatting.
|
||||
*
|
||||
* @returns {Object} A set of functions for message formatting.
|
||||
*/
|
||||
export const useMessageFormatter = () => {
|
||||
/**
|
||||
* Formats a message based on specified conditions.
|
||||
*
|
||||
* @param {string} message - The message to be formatted.
|
||||
* @param {boolean} isATweet - Whether the message is a tweet.
|
||||
* @param {boolean} isAPrivateNote - Whether the message is a private note.
|
||||
* @returns {string} - The formatted message.
|
||||
*/
|
||||
const formatMessage = (message, isATweet, isAPrivateNote) => {
|
||||
const messageFormatter = new MessageFormatter(
|
||||
message,
|
||||
isATweet,
|
||||
isAPrivateNote
|
||||
);
|
||||
return messageFormatter.formattedMessage;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a message to plain text.
|
||||
*
|
||||
* @param {string} message - The message to be converted.
|
||||
* @param {boolean} isATweet - Whether the message is a tweet.
|
||||
* @returns {string} - The plain text message.
|
||||
*/
|
||||
const getPlainText = (message, isATweet) => {
|
||||
const messageFormatter = new MessageFormatter(message, isATweet);
|
||||
return messageFormatter.plainText;
|
||||
};
|
||||
|
||||
/**
|
||||
* Truncates a description to a maximum length of 100 characters.
|
||||
*
|
||||
* @param {string} [description=''] - The description to be truncated.
|
||||
* @returns {string} - The truncated description.
|
||||
*/
|
||||
const truncateMessage = (description = '') => {
|
||||
if (description.length < 100) {
|
||||
return description;
|
||||
}
|
||||
|
||||
return `${description.slice(0, 97)}...`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Highlights occurrences of a search term within given content.
|
||||
*
|
||||
* @param {string} [content=''] - The content in which to search.
|
||||
* @param {string} [searchTerm=''] - The term to search for.
|
||||
* @param {string} [highlightClass=''] - The CSS class to apply to the highlighted term.
|
||||
* @returns {string} - The content with highlighted terms.
|
||||
*/
|
||||
const highlightContent = (
|
||||
content = '',
|
||||
searchTerm = '',
|
||||
highlightClass = ''
|
||||
) => {
|
||||
const plainTextContent = getPlainText(content);
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
|
||||
const escapedSearchTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
return plainTextContent.replace(
|
||||
new RegExp(`(${escapedSearchTerm})`, 'ig'),
|
||||
`<span class="${highlightClass}">$1</span>`
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
formatMessage,
|
||||
getPlainText,
|
||||
truncateMessage,
|
||||
highlightContent,
|
||||
};
|
||||
};
|
||||
@@ -1,40 +0,0 @@
|
||||
import MessageFormatter from '../helpers/MessageFormatter';
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
formatMessage(message, isATweet, isAPrivateNote) {
|
||||
const messageFormatter = new MessageFormatter(
|
||||
message,
|
||||
isATweet,
|
||||
isAPrivateNote
|
||||
);
|
||||
return messageFormatter.formattedMessage;
|
||||
},
|
||||
getPlainText(message, isATweet) {
|
||||
const messageFormatter = new MessageFormatter(message, isATweet);
|
||||
return messageFormatter.plainText;
|
||||
},
|
||||
truncateMessage(description = '') {
|
||||
if (description.length < 100) {
|
||||
return description;
|
||||
}
|
||||
|
||||
return `${description.slice(0, 97)}...`;
|
||||
},
|
||||
|
||||
highlightContent(content = '', searchTerm = '', highlightClass = '') {
|
||||
const plainTextContent = this.getPlainText(content);
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
|
||||
const escapedSearchTerm = searchTerm.replace(
|
||||
/[.*+?^${}()|[\]\\]/g,
|
||||
'\\$&'
|
||||
);
|
||||
|
||||
return plainTextContent.replace(
|
||||
new RegExp(`(${escapedSearchTerm})`, 'ig'),
|
||||
`<span class="${highlightClass}">$1</span>`
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import messageFormatterMixin from '../messageFormatterMixin';
|
||||
|
||||
describe('messageFormatterMixin', () => {
|
||||
it('returns correct plain text', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
mixins: [messageFormatterMixin],
|
||||
};
|
||||
const wrapper = shallowMount(Component);
|
||||
const message =
|
||||
'<b>Chatwoot is an opensource tool. https://www.chatwoot.com</b>';
|
||||
expect(wrapper.vm.getPlainText(message)).toMatch(
|
||||
'Chatwoot is an opensource tool. https://www.chatwoot.com'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import PlaygroundHeader from '../../components/playground/Header.vue';
|
||||
import UserMessage from '../../components/playground/UserMessage.vue';
|
||||
import BotMessage from '../../components/playground/BotMessage.vue';
|
||||
@@ -12,13 +12,18 @@ export default {
|
||||
BotMessage,
|
||||
TypingIndicator,
|
||||
},
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
componentData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
return {
|
||||
formatMessage,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return { messages: [], messageContent: '', isWaiting: false };
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import ChatCard from 'shared/components/ChatCard.vue';
|
||||
import ChatForm from 'shared/components/ChatForm.vue';
|
||||
import ChatOptions from 'shared/components/ChatOptions.vue';
|
||||
@@ -20,7 +20,7 @@ export default {
|
||||
CustomerSatisfaction,
|
||||
IntegrationCard,
|
||||
},
|
||||
mixins: [messageFormatterMixin, darkModeMixin],
|
||||
mixins: [darkModeMixin],
|
||||
props: {
|
||||
message: { type: String, default: null },
|
||||
contentType: { type: String, default: null },
|
||||
@@ -31,6 +31,16 @@ export default {
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { formatMessage, getPlainText, truncateMessage, highlightContent } =
|
||||
useMessageFormatter();
|
||||
return {
|
||||
formatMessage,
|
||||
getPlainText,
|
||||
truncateMessage,
|
||||
highlightContent,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isTemplate() {
|
||||
return this.messageType === 3;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { mapGetters } from 'vuex';
|
||||
import { getContrastingTextColor } from '@chatwoot/utils';
|
||||
import { isEmptyObject } from 'widget/helpers/utils';
|
||||
import { getRegexp } from 'shared/helpers/Validators';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import routerMixin from 'widget/mixins/routerMixin';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin';
|
||||
import configMixin from 'widget/mixins/configMixin';
|
||||
@@ -15,13 +15,19 @@ export default {
|
||||
CustomButton,
|
||||
Spinner,
|
||||
},
|
||||
mixins: [routerMixin, darkModeMixin, messageFormatterMixin, configMixin],
|
||||
mixins: [routerMixin, darkModeMixin, configMixin],
|
||||
props: {
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
return {
|
||||
formatMessage,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
locale: this.$root.$i18n.locale,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import configMixin from '../mixins/configMixin';
|
||||
import { isEmptyObject } from 'widget/helpers/utils';
|
||||
@@ -11,7 +11,7 @@ import darkModeMixin from 'widget/mixins/darkModeMixin';
|
||||
export default {
|
||||
name: 'UnreadMessage',
|
||||
components: { Thumbnail },
|
||||
mixins: [messageFormatterMixin, configMixin, darkModeMixin],
|
||||
mixins: [configMixin, darkModeMixin],
|
||||
props: {
|
||||
message: {
|
||||
type: String,
|
||||
@@ -30,6 +30,12 @@ export default {
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
return {
|
||||
formatMessage,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
companyName() {
|
||||
return `${this.$t('UNREAD_VIEW.COMPANY_FROM')} ${
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<script>
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import { getContrastingTextColor } from '@chatwoot/utils';
|
||||
|
||||
export default {
|
||||
name: 'UserMessageBubble',
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
message: {
|
||||
type: String,
|
||||
@@ -15,6 +14,12 @@ export default {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
return {
|
||||
formatMessage,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
textColor() {
|
||||
return getContrastingTextColor(this.widgetColor);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
|
||||
|
||||
@@ -7,13 +7,19 @@ export default {
|
||||
components: {
|
||||
FluentIcon,
|
||||
},
|
||||
mixins: [messageFormatterMixin, darkModeMixin],
|
||||
mixins: [darkModeMixin],
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { truncateMessage } = useMessageFormatter();
|
||||
return {
|
||||
truncateMessage,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user