Merge branch 'develop' into feat/captain-editor-integration
This commit is contained in:
@@ -48,6 +48,7 @@ const mockStore = createStore({
|
||||
12: { id: 12, channel_type: INBOX_TYPES.SMS },
|
||||
13: { id: 13, channel_type: INBOX_TYPES.INSTAGRAM },
|
||||
14: { id: 14, channel_type: INBOX_TYPES.VOICE },
|
||||
15: { id: 15, channel_type: INBOX_TYPES.TIKTOK },
|
||||
};
|
||||
return inboxes[id] || null;
|
||||
},
|
||||
@@ -215,6 +216,12 @@ describe('useInbox', () => {
|
||||
global: { plugins: [mockStore] },
|
||||
});
|
||||
expect(wrapper.vm.isAVoiceChannel).toBe(true);
|
||||
|
||||
// Test Tiktok
|
||||
wrapper = mount(createTestComponent(15), {
|
||||
global: { plugins: [mockStore] },
|
||||
});
|
||||
expect(wrapper.vm.isATiktokChannel).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -266,6 +273,7 @@ describe('useInbox', () => {
|
||||
'is360DialogWhatsAppChannel',
|
||||
'isAnEmailChannel',
|
||||
'isAnInstagramChannel',
|
||||
'isATiktokChannel',
|
||||
'isAVoiceChannel',
|
||||
];
|
||||
|
||||
|
||||
@@ -1,39 +1,31 @@
|
||||
import { ref } from 'vue';
|
||||
import { useTranslations } from '../useTranslations';
|
||||
import { selectTranslation } from '../useTranslations';
|
||||
|
||||
describe('useTranslations', () => {
|
||||
it('returns false and null when contentAttributes is null', () => {
|
||||
const contentAttributes = ref(null);
|
||||
const { hasTranslations, translationContent } =
|
||||
useTranslations(contentAttributes);
|
||||
expect(hasTranslations.value).toBe(false);
|
||||
expect(translationContent.value).toBeNull();
|
||||
describe('selectTranslation', () => {
|
||||
it('returns null when translations is null', () => {
|
||||
expect(selectTranslation(null, 'en', 'en')).toBeNull();
|
||||
});
|
||||
|
||||
it('returns false and null when translations are missing', () => {
|
||||
const contentAttributes = ref({});
|
||||
const { hasTranslations, translationContent } =
|
||||
useTranslations(contentAttributes);
|
||||
expect(hasTranslations.value).toBe(false);
|
||||
expect(translationContent.value).toBeNull();
|
||||
it('returns null when translations is empty', () => {
|
||||
expect(selectTranslation({}, 'en', 'en')).toBeNull();
|
||||
});
|
||||
|
||||
it('returns false and null when translations is an empty object', () => {
|
||||
const contentAttributes = ref({ translations: {} });
|
||||
const { hasTranslations, translationContent } =
|
||||
useTranslations(contentAttributes);
|
||||
expect(hasTranslations.value).toBe(false);
|
||||
expect(translationContent.value).toBeNull();
|
||||
it('returns first translation when no locale matches', () => {
|
||||
const translations = { en: 'Hello', es: 'Hola' };
|
||||
expect(selectTranslation(translations, 'fr', 'de')).toBe('Hello');
|
||||
});
|
||||
|
||||
it('returns true and correct translation content when translations exist', () => {
|
||||
const contentAttributes = ref({
|
||||
translations: { en: 'Hello' },
|
||||
});
|
||||
const { hasTranslations, translationContent } =
|
||||
useTranslations(contentAttributes);
|
||||
expect(hasTranslations.value).toBe(true);
|
||||
// Should return the first translation (en: 'Hello')
|
||||
expect(translationContent.value).toBe('Hello');
|
||||
it('returns translation matching agent locale', () => {
|
||||
const translations = { en: 'Hello', es: 'Hola', zh_CN: '你好' };
|
||||
expect(selectTranslation(translations, 'es', 'en')).toBe('Hola');
|
||||
});
|
||||
|
||||
it('falls back to account locale when agent locale not found', () => {
|
||||
const translations = { en: 'Hello', zh_CN: '你好' };
|
||||
expect(selectTranslation(translations, 'fr', 'zh_CN')).toBe('你好');
|
||||
});
|
||||
|
||||
it('returns first translation when both locales are undefined', () => {
|
||||
const translations = { en: 'Hello', es: 'Hola' };
|
||||
expect(selectTranslation(translations, undefined, undefined)).toBe('Hello');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { computed, ref, watch, onUnmounted, onMounted } from 'vue';
|
||||
import VoiceAPI from 'dashboard/api/channel/voice/voiceAPIClient';
|
||||
import TwilioVoiceClient from 'dashboard/api/channel/voice/twilioVoiceClient';
|
||||
import { useCallsStore } from 'dashboard/stores/calls';
|
||||
import Timer from 'dashboard/helper/Timer';
|
||||
|
||||
export function useCallSession() {
|
||||
const callsStore = useCallsStore();
|
||||
const isJoining = ref(false);
|
||||
const callDuration = ref(0);
|
||||
const durationTimer = new Timer(elapsed => {
|
||||
callDuration.value = elapsed;
|
||||
});
|
||||
|
||||
const activeCall = computed(() => callsStore.activeCall);
|
||||
const incomingCalls = computed(() => callsStore.incomingCalls);
|
||||
const hasActiveCall = computed(() => callsStore.hasActiveCall);
|
||||
|
||||
watch(
|
||||
hasActiveCall,
|
||||
active => {
|
||||
if (active) {
|
||||
durationTimer.start();
|
||||
} else {
|
||||
durationTimer.stop();
|
||||
callDuration.value = 0;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
TwilioVoiceClient.addEventListener('call:disconnected', () =>
|
||||
callsStore.clearActiveCall()
|
||||
);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
durationTimer.stop();
|
||||
TwilioVoiceClient.removeEventListener('call:disconnected', () =>
|
||||
callsStore.clearActiveCall()
|
||||
);
|
||||
});
|
||||
|
||||
const endCall = async ({ conversationId, inboxId }) => {
|
||||
await VoiceAPI.leaveConference(inboxId, conversationId);
|
||||
TwilioVoiceClient.endClientCall();
|
||||
durationTimer.stop();
|
||||
callsStore.clearActiveCall();
|
||||
};
|
||||
|
||||
const joinCall = async ({ conversationId, inboxId, callSid }) => {
|
||||
if (isJoining.value) return null;
|
||||
|
||||
isJoining.value = true;
|
||||
try {
|
||||
const device = await TwilioVoiceClient.initializeDevice(inboxId);
|
||||
if (!device) return null;
|
||||
|
||||
const joinResponse = await VoiceAPI.joinConference({
|
||||
conversationId,
|
||||
inboxId,
|
||||
callSid,
|
||||
});
|
||||
|
||||
await TwilioVoiceClient.joinClientCall({
|
||||
to: joinResponse?.conference_sid,
|
||||
conversationId,
|
||||
});
|
||||
|
||||
callsStore.setCallActive(callSid);
|
||||
durationTimer.start();
|
||||
|
||||
return { conferenceSid: joinResponse?.conference_sid };
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to join call:', error);
|
||||
return null;
|
||||
} finally {
|
||||
isJoining.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const rejectIncomingCall = callSid => {
|
||||
TwilioVoiceClient.endClientCall();
|
||||
callsStore.dismissCall(callSid);
|
||||
};
|
||||
|
||||
const dismissCall = callSid => {
|
||||
callsStore.dismissCall(callSid);
|
||||
};
|
||||
|
||||
const formattedCallDuration = computed(() => {
|
||||
const minutes = Math.floor(callDuration.value / 60);
|
||||
const seconds = callDuration.value % 60;
|
||||
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
});
|
||||
|
||||
return {
|
||||
activeCall,
|
||||
incomingCalls,
|
||||
hasActiveCall,
|
||||
isJoining,
|
||||
formattedCallDuration,
|
||||
joinCall,
|
||||
endCall,
|
||||
rejectIncomingCall,
|
||||
dismissCall,
|
||||
};
|
||||
}
|
||||
@@ -17,6 +17,7 @@ export const INBOX_FEATURE_MAP = {
|
||||
INBOX_TYPES.TWITTER,
|
||||
INBOX_TYPES.WHATSAPP,
|
||||
INBOX_TYPES.TELEGRAM,
|
||||
INBOX_TYPES.TIKTOK,
|
||||
INBOX_TYPES.API,
|
||||
],
|
||||
[INBOX_FEATURES.REPLY_TO_OUTGOING]: [
|
||||
@@ -24,6 +25,7 @@ export const INBOX_FEATURE_MAP = {
|
||||
INBOX_TYPES.TWITTER,
|
||||
INBOX_TYPES.WHATSAPP,
|
||||
INBOX_TYPES.TELEGRAM,
|
||||
INBOX_TYPES.TIKTOK,
|
||||
INBOX_TYPES.API,
|
||||
],
|
||||
};
|
||||
@@ -128,6 +130,10 @@ export const useInbox = (inboxId = null) => {
|
||||
return channelType.value === INBOX_TYPES.INSTAGRAM;
|
||||
});
|
||||
|
||||
const isATiktokChannel = computed(() => {
|
||||
return channelType.value === INBOX_TYPES.TIKTOK;
|
||||
});
|
||||
|
||||
const isAVoiceChannel = computed(() => {
|
||||
return channelType.value === INBOX_TYPES.VOICE;
|
||||
});
|
||||
@@ -149,6 +155,7 @@ export const useInbox = (inboxId = null) => {
|
||||
is360DialogWhatsAppChannel,
|
||||
isAnEmailChannel,
|
||||
isAnInstagramChannel,
|
||||
isATiktokChannel,
|
||||
isAVoiceChannel,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,25 @@
|
||||
import { computed } from 'vue';
|
||||
import { useUISettings } from './useUISettings';
|
||||
import { useAccount } from './useAccount';
|
||||
|
||||
/**
|
||||
* Select translation based on locale priority.
|
||||
* @param {Object} translations - Translations object with locale keys
|
||||
* @param {string} agentLocale - Agent's preferred locale
|
||||
* @param {string} accountLocale - Account's default locale
|
||||
* @returns {string|null} Selected translation or null
|
||||
*/
|
||||
export function selectTranslation(translations, agentLocale, accountLocale) {
|
||||
if (!translations || Object.keys(translations).length === 0) return null;
|
||||
|
||||
if (agentLocale && translations[agentLocale]) {
|
||||
return translations[agentLocale];
|
||||
}
|
||||
if (accountLocale && translations[accountLocale]) {
|
||||
return translations[accountLocale];
|
||||
}
|
||||
return translations[Object.keys(translations)[0]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable to extract translation state/content from contentAttributes.
|
||||
@@ -6,6 +27,9 @@ import { computed } from 'vue';
|
||||
* @returns {Object} { hasTranslations, translationContent }
|
||||
*/
|
||||
export function useTranslations(contentAttributes) {
|
||||
const { uiSettings } = useUISettings();
|
||||
const { currentAccount } = useAccount();
|
||||
|
||||
const hasTranslations = computed(() => {
|
||||
if (!contentAttributes.value) return false;
|
||||
const { translations = {} } = contentAttributes.value;
|
||||
@@ -14,8 +38,11 @@ export function useTranslations(contentAttributes) {
|
||||
|
||||
const translationContent = computed(() => {
|
||||
if (!hasTranslations.value) return null;
|
||||
const translations = contentAttributes.value.translations;
|
||||
return translations[Object.keys(translations)[0]];
|
||||
return selectTranslation(
|
||||
contentAttributes.value.translations,
|
||||
uiSettings.value?.locale,
|
||||
currentAccount.value?.locale
|
||||
);
|
||||
});
|
||||
|
||||
return { hasTranslations, translationContent };
|
||||
|
||||
Reference in New Issue
Block a user