Merge branch 'develop' into feature/10945-search-results-page
This commit is contained in:
@@ -137,6 +137,10 @@ class ConversationApi extends ApiClient {
|
||||
getInboxAssistant(conversationId) {
|
||||
return axios.get(`${this.url}/${conversationId}/inbox_assistant`);
|
||||
}
|
||||
|
||||
delete(conversationId) {
|
||||
return axios.delete(`${this.url}/${conversationId}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new ConversationApi();
|
||||
|
||||
@@ -4,38 +4,14 @@ import { computed, ref, watch, useSlots } from 'vue';
|
||||
import WootEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
focusOnMount: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
maxLength: {
|
||||
type: Number,
|
||||
default: 200,
|
||||
},
|
||||
showCharacterCount: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
modelValue: { type: String, default: '' },
|
||||
label: { type: String, default: '' },
|
||||
placeholder: { type: String, default: '' },
|
||||
focusOnMount: { type: Boolean, default: false },
|
||||
maxLength: { type: Number, default: 200 },
|
||||
showCharacterCount: { type: Boolean, default: true },
|
||||
disabled: { type: Boolean, default: false },
|
||||
message: { type: String, default: '' },
|
||||
messageType: {
|
||||
type: String,
|
||||
default: 'info',
|
||||
@@ -43,6 +19,7 @@ const props = defineProps({
|
||||
},
|
||||
enableVariables: { type: Boolean, default: false },
|
||||
enableCannedResponses: { type: Boolean, default: true },
|
||||
enabledMenuOptions: { type: Array, default: () => [] },
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
@@ -120,6 +97,7 @@ watch(
|
||||
:disabled="disabled"
|
||||
:enable-variables="enableVariables"
|
||||
:enable-canned-responses="enableCannedResponses"
|
||||
:enabled-menu-options="enabledMenuOptions"
|
||||
@input="handleInput"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
|
||||
@@ -109,49 +109,58 @@ const downloadAudio = async () => {
|
||||
</audio>
|
||||
<div
|
||||
v-bind="$attrs"
|
||||
class="rounded-xl w-full gap-1 p-1.5 bg-n-alpha-white flex items-center border border-n-container shadow-[0px_2px_8px_0px_rgba(94,94,94,0.06)]"
|
||||
class="rounded-xl w-full gap-2 p-1.5 bg-n-alpha-white flex flex-col items-center border border-n-container shadow-[0px_2px_8px_0px_rgba(94,94,94,0.06)]"
|
||||
>
|
||||
<button class="p-0 border-0 size-8" @click="playOrPause">
|
||||
<Icon
|
||||
v-if="isPlaying"
|
||||
class="size-8"
|
||||
icon="i-teenyicons-pause-small-solid"
|
||||
/>
|
||||
<Icon v-else class="size-8" icon="i-teenyicons-play-small-solid" />
|
||||
</button>
|
||||
<div class="tabular-nums text-xs">
|
||||
{{ formatTime(currentTime) }} / {{ formatTime(duration) }}
|
||||
<div class="flex gap-1 w-full flex-1 items-center justify-start">
|
||||
<button class="p-0 border-0 size-8" @click="playOrPause">
|
||||
<Icon
|
||||
v-if="isPlaying"
|
||||
class="size-8"
|
||||
icon="i-teenyicons-pause-small-solid"
|
||||
/>
|
||||
<Icon v-else class="size-8" icon="i-teenyicons-play-small-solid" />
|
||||
</button>
|
||||
<div class="tabular-nums text-xs">
|
||||
{{ formatTime(currentTime) }} / {{ formatTime(duration) }}
|
||||
</div>
|
||||
<div class="flex-1 items-center flex px-2">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
:max="duration"
|
||||
:value="currentTime"
|
||||
class="w-full h-1 bg-n-slate-12/40 rounded-lg appearance-none cursor-pointer accent-current"
|
||||
@input="seek"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="border-0 w-10 h-6 grid place-content-center bg-n-alpha-2 hover:bg-alpha-3 rounded-2xl"
|
||||
@click="changePlaybackSpeed"
|
||||
>
|
||||
<span class="text-xs text-n-slate-11 font-medium">
|
||||
{{ playbackSpeedLabel }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="p-0 border-0 size-8 grid place-content-center"
|
||||
@click="toggleMute"
|
||||
>
|
||||
<Icon v-if="isMuted" class="size-4" icon="i-lucide-volume-off" />
|
||||
<Icon v-else class="size-4" icon="i-lucide-volume-2" />
|
||||
</button>
|
||||
<button
|
||||
class="p-0 border-0 size-8 grid place-content-center"
|
||||
@click="downloadAudio"
|
||||
>
|
||||
<Icon class="size-4" icon="i-lucide-download" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1 items-center flex px-2">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
:max="duration"
|
||||
:value="currentTime"
|
||||
class="w-full h-1 bg-n-slate-12/40 rounded-lg appearance-none cursor-pointer accent-current"
|
||||
@input="seek"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="attachment.transcribedText"
|
||||
class="text-n-slate-12 p-3 text-sm bg-n-alpha-1 rounded-lg w-full break-words"
|
||||
>
|
||||
{{ attachment.transcribedText }}
|
||||
</div>
|
||||
<button
|
||||
class="border-0 w-10 h-6 grid place-content-center bg-n-alpha-2 hover:bg-alpha-3 rounded-2xl"
|
||||
@click="changePlaybackSpeed"
|
||||
>
|
||||
<span class="text-xs text-n-slate-11 font-medium">
|
||||
{{ playbackSpeedLabel }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="p-0 border-0 size-8 grid place-content-center"
|
||||
@click="toggleMute"
|
||||
>
|
||||
<Icon v-if="isMuted" class="size-4" icon="i-lucide-volume-off" />
|
||||
<Icon v-else class="size-4" icon="i-lucide-volume-2" />
|
||||
</button>
|
||||
<button
|
||||
class="p-0 border-0 size-8 grid place-content-center"
|
||||
@click="downloadAudio"
|
||||
>
|
||||
<Icon class="size-4" icon="i-lucide-download" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
// https://tanstack.com/virtual/latest/docs/framework/vue/examples/variable
|
||||
import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller';
|
||||
import ChatListHeader from './ChatListHeader.vue';
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
import ConversationFilter from 'next/filter/ConversationFilter.vue';
|
||||
import SaveCustomView from 'next/filter/SaveCustomView.vue';
|
||||
import ChatTypeTabs from './widgets/ChatTypeTabs.vue';
|
||||
@@ -82,6 +83,7 @@ const emit = defineEmits(['conversationLoad']);
|
||||
const { uiSettings } = useUISettings();
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const store = useStore();
|
||||
|
||||
const conversationListRef = ref(null);
|
||||
@@ -646,6 +648,30 @@ function openLastItemAfterDeleteInFolder() {
|
||||
}
|
||||
}
|
||||
|
||||
function redirectToConversationList() {
|
||||
const {
|
||||
params: { accountId, inbox_id: inboxId, label, teamId },
|
||||
name,
|
||||
} = route;
|
||||
|
||||
let conversationType = '';
|
||||
if (isOnMentionsView({ route: { name } })) {
|
||||
conversationType = 'mention';
|
||||
} else if (isOnUnattendedView({ route: { name } })) {
|
||||
conversationType = 'unattended';
|
||||
}
|
||||
router.push(
|
||||
conversationListPageURL({
|
||||
accountId,
|
||||
conversationType: conversationType,
|
||||
customViewId: props.foldersId,
|
||||
inboxId,
|
||||
label,
|
||||
teamId,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function assignPriority(priority, conversationId = null) {
|
||||
store.dispatch('setCurrentChatPriority', {
|
||||
priority,
|
||||
@@ -670,26 +696,7 @@ async function markAsUnread(conversationId) {
|
||||
await store.dispatch('markMessagesUnread', {
|
||||
id: conversationId,
|
||||
});
|
||||
const {
|
||||
params: { accountId, inbox_id: inboxId, label, teamId },
|
||||
name,
|
||||
} = useRoute();
|
||||
let conversationType = '';
|
||||
if (isOnMentionsView({ route: { name } })) {
|
||||
conversationType = 'mention';
|
||||
} else if (isOnUnattendedView({ route: { name } })) {
|
||||
conversationType = 'unattended';
|
||||
}
|
||||
router.push(
|
||||
conversationListPageURL({
|
||||
accountId,
|
||||
conversationType: conversationType,
|
||||
customViewId: props.foldersId,
|
||||
inboxId,
|
||||
label,
|
||||
teamId,
|
||||
})
|
||||
);
|
||||
redirectToConversationList();
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
}
|
||||
@@ -703,6 +710,7 @@ async function markAsRead(conversationId) {
|
||||
// Ignore error
|
||||
}
|
||||
}
|
||||
|
||||
async function onAssignTeam(team, conversationId = null) {
|
||||
try {
|
||||
await store.dispatch('assignTeam', {
|
||||
@@ -764,6 +772,26 @@ onMounted(() => {
|
||||
}
|
||||
});
|
||||
|
||||
const deleteConversationDialogRef = ref(null);
|
||||
const selectedConversationId = ref(null);
|
||||
|
||||
async function deleteConversation() {
|
||||
try {
|
||||
await store.dispatch('deleteConversation', selectedConversationId.value);
|
||||
redirectToConversationList();
|
||||
selectedConversationId.value = null;
|
||||
deleteConversationDialogRef.value.close();
|
||||
useAlert(t('CONVERSATION.SUCCESS_DELETE_CONVERSATION'));
|
||||
} catch (error) {
|
||||
useAlert(t('CONVERSATION.FAIL_DELETE_CONVERSATION'));
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = conversationId => {
|
||||
selectedConversationId.value = conversationId;
|
||||
deleteConversationDialogRef.value.open();
|
||||
};
|
||||
|
||||
provide('selectConversation', selectConversation);
|
||||
provide('deSelectConversation', deSelectConversation);
|
||||
provide('assignAgent', onAssignAgent);
|
||||
@@ -775,6 +803,7 @@ provide('markAsUnread', markAsUnread);
|
||||
provide('markAsRead', markAsRead);
|
||||
provide('assignPriority', assignPriority);
|
||||
provide('isConversationSelected', isConversationSelected);
|
||||
provide('deleteConversation', handleDelete);
|
||||
|
||||
watch(activeTeam, () => resetAndFetchData());
|
||||
|
||||
@@ -938,6 +967,19 @@ watch(conversationFilters, (newVal, oldVal) => {
|
||||
</template>
|
||||
</DynamicScroller>
|
||||
</div>
|
||||
<Dialog
|
||||
ref="deleteConversationDialogRef"
|
||||
type="alert"
|
||||
:title="
|
||||
$t('CONVERSATION.DELETE_CONVERSATION.TITLE', {
|
||||
conversationId: selectedConversationId,
|
||||
})
|
||||
"
|
||||
:description="$t('CONVERSATION.DELETE_CONVERSATION.DESCRIPTION')"
|
||||
:confirm-button-label="$t('CONVERSATION.DELETE_CONVERSATION.CONFIRM')"
|
||||
@confirm="deleteConversation"
|
||||
@close="selectedConversationId = null"
|
||||
/>
|
||||
<TeleportWithDirection
|
||||
v-if="showAdvancedFilters"
|
||||
to="#conversationFilterTeleportTarget"
|
||||
|
||||
@@ -16,6 +16,7 @@ export default {
|
||||
'markAsRead',
|
||||
'assignPriority',
|
||||
'isConversationSelected',
|
||||
'deleteConversation',
|
||||
],
|
||||
props: {
|
||||
source: {
|
||||
@@ -67,5 +68,6 @@ export default {
|
||||
@mark-as-unread="markAsUnread"
|
||||
@mark-as-read="markAsRead"
|
||||
@assign-priority="assignPriority"
|
||||
@delete-conversation="deleteConversation"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -78,6 +78,7 @@ export default {
|
||||
'markAsRead',
|
||||
'assignPriority',
|
||||
'updateConversationStatus',
|
||||
'deleteConversation',
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
@@ -237,6 +238,10 @@ export default {
|
||||
this.$emit('assignPriority', priority, this.chat.id);
|
||||
this.closeContextMenu();
|
||||
},
|
||||
async deleteConversation() {
|
||||
this.$emit('deleteConversation', this.chat.id);
|
||||
this.closeContextMenu();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -363,6 +368,7 @@ export default {
|
||||
@mark-as-unread="markAsUnread"
|
||||
@mark-as-read="markAsRead"
|
||||
@assign-priority="assignPriority"
|
||||
@delete-conversation="deleteConversation"
|
||||
/>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import MenuItem from './menuItem.vue';
|
||||
import MenuItemWithSubmenu from './menuItemWithSubmenu.vue';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import AgentLoadingPlaceholder from './agentLoadingPlaceholder.vue';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -45,7 +46,14 @@ export default {
|
||||
'assignAgent',
|
||||
'assignTeam',
|
||||
'assignLabel',
|
||||
'deleteConversation',
|
||||
],
|
||||
setup() {
|
||||
const { isAdmin } = useAdmin();
|
||||
return {
|
||||
isAdmin,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
STATUS_TYPE: wootConstants.STATUS_TYPE,
|
||||
@@ -121,6 +129,11 @@ export default {
|
||||
icon: 'people-team-add',
|
||||
label: this.$t('CONVERSATION.CARD_CONTEXT_MENU.ASSIGN_TEAM'),
|
||||
},
|
||||
deleteOption: {
|
||||
key: 'delete',
|
||||
icon: 'delete',
|
||||
label: this.$t('CONVERSATION.CARD_CONTEXT_MENU.DELETE'),
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -178,6 +191,9 @@ export default {
|
||||
assignPriority(priority) {
|
||||
this.$emit('assignPriority', priority);
|
||||
},
|
||||
deleteConversation() {
|
||||
this.$emit('deleteConversation', this.chatId);
|
||||
},
|
||||
show(key) {
|
||||
// If the conversation status is same as the action, then don't display the option
|
||||
// i.e.: Don't show an option to resolve if the conversation is already resolved.
|
||||
@@ -277,5 +293,13 @@ export default {
|
||||
@click.stop="$emit('assignTeam', team)"
|
||||
/>
|
||||
</MenuItemWithSubmenu>
|
||||
<template v-if="isAdmin">
|
||||
<hr class="m-1 rounded border-b border-n-weak dark:border-n-weak" />
|
||||
<MenuItem
|
||||
:option="deleteOption"
|
||||
variant="icon"
|
||||
@click.stop="deleteConversation"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -33,6 +33,14 @@ export const ARTICLE_EDITOR_MENU_OPTIONS = [
|
||||
'code',
|
||||
];
|
||||
|
||||
export const WIDGET_BUILDER_EDITOR_MENU_OPTIONS = [
|
||||
'strong',
|
||||
'em',
|
||||
'link',
|
||||
'undo',
|
||||
'redo',
|
||||
];
|
||||
|
||||
export const MESSAGE_EDITOR_IMAGE_RESIZES = [
|
||||
{
|
||||
name: 'Small',
|
||||
|
||||
@@ -36,6 +36,7 @@ const translationKeys = {
|
||||
'teammember:create': `AUDIT_LOGS.TEAM_MEMBER.ADD`,
|
||||
'teammember:destroy': `AUDIT_LOGS.TEAM_MEMBER.REMOVE`,
|
||||
'account:update': `AUDIT_LOGS.ACCOUNT.EDIT`,
|
||||
'conversation:destroy': `AUDIT_LOGS.CONVERSATION.DELETE`,
|
||||
};
|
||||
|
||||
function extractAttrChange(attrChange) {
|
||||
@@ -168,6 +169,11 @@ export function generateTranslationPayload(auditLogItem, agentList) {
|
||||
const auditableType = auditLogItem.auditable_type.toLowerCase();
|
||||
const action = auditLogItem.action.toLowerCase();
|
||||
|
||||
if (auditableType === 'conversation' && action === 'destroy') {
|
||||
translationPayload.id =
|
||||
auditLogItem.audited_changes?.display_id || auditLogItem.auditable_id;
|
||||
}
|
||||
|
||||
if (auditableType === 'accountuser') {
|
||||
translationPayload = handleAccountUser(
|
||||
auditLogItem,
|
||||
|
||||
@@ -69,6 +69,9 @@
|
||||
},
|
||||
"ACCOUNT": {
|
||||
"EDIT": "{agentName} updated the account configuration (#{id})"
|
||||
},
|
||||
"CONVERSATION": {
|
||||
"DELETE": "{agentName} deleted conversation #{id}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +118,11 @@
|
||||
"FAILED": "Couldn't change priority. Please try again."
|
||||
}
|
||||
},
|
||||
"DELETE_CONVERSATION": {
|
||||
"TITLE": "Delete conversation #{conversationId}",
|
||||
"DESCRIPTION": "Are you sure you want to delete this conversation?",
|
||||
"CONFIRM": "Delete"
|
||||
},
|
||||
"CARD_CONTEXT_MENU": {
|
||||
"PENDING": "Mark as pending",
|
||||
"RESOLVED": "Mark as resolved",
|
||||
@@ -134,6 +139,7 @@
|
||||
"ASSIGN_LABEL": "Assign label",
|
||||
"AGENTS_LOADING": "Loading agents...",
|
||||
"ASSIGN_TEAM": "Assign team",
|
||||
"DELETE": "Delete conversation",
|
||||
"API": {
|
||||
"AGENT_ASSIGNMENT": {
|
||||
"SUCCESFUL": "Conversation id {conversationId} assigned to \"{agentName}\"",
|
||||
@@ -208,6 +214,8 @@
|
||||
"ASSIGN_LABEL_SUCCESFUL": "Label assigned successfully",
|
||||
"ASSIGN_LABEL_FAILED": "Label assignment failed",
|
||||
"CHANGE_TEAM": "Conversation team changed",
|
||||
"SUCCESS_DELETE_CONVERSATION": "Conversation deleted successfully",
|
||||
"FAIL_DELETE_CONVERSATION": "Couldn't delete conversation! Try again",
|
||||
"FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE} MB attachment limit",
|
||||
"MESSAGE_ERROR": "Unable to send this message, please try again later",
|
||||
"SENT_BY": "Sent by:",
|
||||
|
||||
@@ -92,6 +92,32 @@
|
||||
"PLACEHOLDER": "Your company's support email",
|
||||
"ERROR": ""
|
||||
},
|
||||
"AUTO_RESOLVE_IGNORE_WAITING": {
|
||||
"LABEL": "Exclude unattended conversations",
|
||||
"HELP": "When enabled, the system will skip resolving conversations that are still waiting for an agent's reply."
|
||||
},
|
||||
"AUDIO_TRANSCRIPTION": {
|
||||
"TITLE": "Transcribe Audio Messages",
|
||||
"NOTE": "Automatically transcribe audio messages in conversations. Generate a text transcript whenever an audio message is sent or received, and display it alongside the message.",
|
||||
"API": {
|
||||
"SUCCESS": "Audio transcription setting updated successfully",
|
||||
"ERROR": "Failed to update audio transcription setting"
|
||||
}
|
||||
},
|
||||
"AUTO_RESOLVE_DURATION": {
|
||||
"LABEL": "Inactivity duration for resolution",
|
||||
"HELP": "Duration after a conversation should auto resolve if there is no activity",
|
||||
"PLACEHOLDER": "30",
|
||||
"ERROR": "Auto resolve duration should be between 10 minutes and 999 days",
|
||||
"API": {
|
||||
"SUCCESS": "Auto resolve settings updated successfully",
|
||||
"ERROR": "Failed to update auto resolve settings"
|
||||
},
|
||||
"UPDATE_BUTTON": "Update",
|
||||
"MESSAGE_LABEL": "Custom resolution message",
|
||||
"MESSAGE_PLACEHOLDER": "Conversation was marked resolved by system due to 15 days of inactivity",
|
||||
"MESSAGE_HELP": "This message is sent to the customer when a conversation is automatically resolved by the system due to inactivity."
|
||||
},
|
||||
"FEATURES": {
|
||||
"INBOUND_EMAIL_ENABLED": "Conversation continuity with emails is enabled for your account.",
|
||||
"CUSTOM_EMAIL_DOMAIN_ENABLED": "You can receive emails in your custom domain now."
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
|
||||
const props = defineProps({
|
||||
config: {
|
||||
@@ -8,6 +9,8 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
|
||||
const isDefaultScreen = computed(() => {
|
||||
return (
|
||||
props.config.isDefaultScreen &&
|
||||
@@ -53,12 +56,13 @@ const isDefaultScreen = computed(() => {
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isDefaultScreen" class="overflow-auto max-h-60">
|
||||
<h2 class="mb-2 text-2xl break-words text-slate-900 dark:text-white">
|
||||
<h2 class="mb-2 text-2xl break-words text-n-slate-12">
|
||||
{{ config.welcomeHeading }}
|
||||
</h2>
|
||||
<p class="text-sm break-words text-slate-600 dark:text-slate-100">
|
||||
{{ config.welcomeTagline }}
|
||||
</p>
|
||||
<p
|
||||
v-dompurify-html="formatMessage(config.welcomeTagline)"
|
||||
class="text-sm break-words text-n-slate-11 [&_a]:!text-n-slate-11 [&_a]:underline"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,7 @@ import AccountId from './components/AccountId.vue';
|
||||
import BuildInfo from './components/BuildInfo.vue';
|
||||
import AccountDelete from './components/AccountDelete.vue';
|
||||
import AutoResolve from './components/AutoResolve.vue';
|
||||
import AudioTranscription from './components/AudioTranscription.vue';
|
||||
import SectionLayout from './components/SectionLayout.vue';
|
||||
|
||||
export default {
|
||||
@@ -26,6 +27,7 @@ export default {
|
||||
BuildInfo,
|
||||
AccountDelete,
|
||||
AutoResolve,
|
||||
AudioTranscription,
|
||||
SectionLayout,
|
||||
WithLabel,
|
||||
NextInput,
|
||||
@@ -235,6 +237,7 @@ export default {
|
||||
<woot-loading-state v-if="uiFlags.isFetchingItem" />
|
||||
</div>
|
||||
<AutoResolve v-if="showAutoResolutionConfig" />
|
||||
<AudioTranscription v-if="isOnChatwootCloud" />
|
||||
<AccountId />
|
||||
<div v-if="!uiFlags.isFetchingItem && isOnChatwootCloud">
|
||||
<AccountDelete />
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import SectionLayout from './SectionLayout.vue';
|
||||
import Switch from 'next/switch/Switch.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const isEnabled = ref(false);
|
||||
|
||||
const { currentAccount, updateAccount } = useAccount();
|
||||
|
||||
watch(
|
||||
currentAccount,
|
||||
() => {
|
||||
const { audio_transcriptions } = currentAccount.value?.settings || {};
|
||||
isEnabled.value = !!audio_transcriptions;
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
const updateAccountSettings = async settings => {
|
||||
try {
|
||||
await updateAccount(settings);
|
||||
useAlert(t('GENERAL_SETTINGS.FORM.AUDIO_TRANSCRIPTION.API.SUCCESS'));
|
||||
} catch (error) {
|
||||
useAlert(t('GENERAL_SETTINGS.FORM.AUDIO_TRANSCRIPTION.API.ERROR'));
|
||||
}
|
||||
};
|
||||
|
||||
const toggleAudioTranscription = async () => {
|
||||
return updateAccountSettings({
|
||||
audio_transcriptions: isEnabled.value,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SectionLayout
|
||||
:title="t('GENERAL_SETTINGS.FORM.AUDIO_TRANSCRIPTION.TITLE')"
|
||||
:description="t('GENERAL_SETTINGS.FORM.AUDIO_TRANSCRIPTION.NOTE')"
|
||||
with-border
|
||||
>
|
||||
<template #headerActions>
|
||||
<div class="flex justify-end">
|
||||
<Switch v-model="isEnabled" @change="toggleAudioTranscription" />
|
||||
</div>
|
||||
</template>
|
||||
</SectionLayout>
|
||||
</template>
|
||||
@@ -23,6 +23,8 @@ import { FEATURE_FLAGS } from '../../../../featureFlags';
|
||||
import SenderNameExamplePreview from './components/SenderNameExamplePreview.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import { INBOX_TYPES } from 'dashboard/helper/inbox';
|
||||
import { WIDGET_BUILDER_EDITOR_MENU_OPTIONS } from 'dashboard/constants/editor';
|
||||
import Editor from 'dashboard/components-next/Editor/Editor.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -43,6 +45,7 @@ export default {
|
||||
NextButton,
|
||||
InstagramReauthorize,
|
||||
DuplicateInboxBanner,
|
||||
Editor,
|
||||
},
|
||||
mixins: [inboxMixin],
|
||||
setup() {
|
||||
@@ -70,6 +73,7 @@ export default {
|
||||
selectedTabIndex: 0,
|
||||
selectedPortalSlug: '',
|
||||
showBusinessNameInput: false,
|
||||
welcomeTaglineEditorMenuOptions: WIDGET_BUILDER_EDITOR_MENU_OPTIONS,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -480,10 +484,10 @@ export default {
|
||||
"
|
||||
/>
|
||||
|
||||
<woot-input
|
||||
<Editor
|
||||
v-if="isAWebWidgetInbox"
|
||||
v-model="channelWelcomeTagline"
|
||||
class="pb-4"
|
||||
class="mb-4"
|
||||
:label="
|
||||
$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WELCOME_TAGLINE.LABEL')
|
||||
"
|
||||
@@ -492,6 +496,8 @@ export default {
|
||||
'INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WELCOME_TAGLINE.PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
:max-length="255"
|
||||
:enabled-menu-options="welcomeTaglineEditorMenuOptions"
|
||||
/>
|
||||
|
||||
<label v-if="isAWebWidgetInbox" class="pb-4">
|
||||
|
||||
@@ -7,13 +7,16 @@ import { useVuelidate } from '@vuelidate/core';
|
||||
import { required } from '@vuelidate/validators';
|
||||
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||
import { WIDGET_BUILDER_EDITOR_MENU_OPTIONS } from 'dashboard/constants/editor';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import Editor from 'dashboard/components-next/Editor/Editor.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Widget,
|
||||
InputRadioGroup,
|
||||
NextButton,
|
||||
Editor,
|
||||
},
|
||||
props: {
|
||||
inbox: {
|
||||
@@ -71,6 +74,7 @@ export default {
|
||||
checked: false,
|
||||
},
|
||||
],
|
||||
welcomeTaglineEditorMenuOptions: WIDGET_BUILDER_EDITOR_MENU_OPTIONS,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -310,7 +314,7 @@ export default {
|
||||
)
|
||||
"
|
||||
/>
|
||||
<woot-input
|
||||
<Editor
|
||||
v-model="welcomeTagline"
|
||||
:label="
|
||||
$t(
|
||||
@@ -322,6 +326,9 @@ export default {
|
||||
'INBOX_MGMT.WIDGET_BUILDER.WIDGET_OPTIONS.WELCOME_TAGLINE.PLACE_HOLDER'
|
||||
)
|
||||
"
|
||||
:max-length="255"
|
||||
:enabled-menu-options="welcomeTaglineEditorMenuOptions"
|
||||
class="mb-4"
|
||||
/>
|
||||
<label>
|
||||
{{
|
||||
|
||||
@@ -5,12 +5,15 @@ import router from '../../../../index';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import PageHeader from '../../SettingsSubPageHeader.vue';
|
||||
import GreetingsEditor from 'shared/components/GreetingsEditor.vue';
|
||||
import { WIDGET_BUILDER_EDITOR_MENU_OPTIONS } from 'dashboard/constants/editor';
|
||||
import Editor from 'dashboard/components-next/Editor/Editor.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PageHeader,
|
||||
GreetingsEditor,
|
||||
NextButton,
|
||||
Editor,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -21,6 +24,7 @@ export default {
|
||||
channelWelcomeTagline: '',
|
||||
greetingEnabled: false,
|
||||
greetingMessage: '',
|
||||
welcomeTaglineEditorMenuOptions: WIDGET_BUILDER_EDITOR_MENU_OPTIONS,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -134,22 +138,21 @@ export default {
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<label>
|
||||
{{
|
||||
$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WELCOME_TAGLINE.LABEL')
|
||||
}}
|
||||
<input
|
||||
v-model="channelWelcomeTagline"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t(
|
||||
'INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WELCOME_TAGLINE.PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<Editor
|
||||
v-model="channelWelcomeTagline"
|
||||
:label="
|
||||
$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WELCOME_TAGLINE.LABEL')
|
||||
"
|
||||
:placeholder="
|
||||
$t(
|
||||
'INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WELCOME_TAGLINE.PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
:max-length="255"
|
||||
:enabled-menu-options="welcomeTaglineEditorMenuOptions"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<label class="w-full">
|
||||
{{ $t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_GREETING_TOGGLE.LABEL') }}
|
||||
<select v-model="greetingEnabled">
|
||||
|
||||
@@ -327,6 +327,16 @@ const actions = {
|
||||
}
|
||||
},
|
||||
|
||||
deleteConversation: async ({ commit, dispatch }, conversationId) => {
|
||||
try {
|
||||
await ConversationApi.delete(conversationId);
|
||||
commit(types.DELETE_CONVERSATION, conversationId);
|
||||
dispatch('conversationStats/get', {}, { root: true });
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
},
|
||||
|
||||
addConversation({ commit, state, dispatch, rootState }, conversation) {
|
||||
const { currentInbox, appliedFilters } = state;
|
||||
const {
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
* 3. Nested properties in custom_attributes (conversation_type, etc.)
|
||||
*/
|
||||
import jsonLogic from 'json-logic-js';
|
||||
import { coerceToDate } from '@chatwoot/utils';
|
||||
|
||||
/**
|
||||
* Gets a value from a conversation based on the attribute key
|
||||
@@ -157,6 +158,20 @@ const contains = (filterValue, conversationValue) => {
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Compares two date values using a comparison function
|
||||
* @param {*} conversationValue - The conversation value to compare
|
||||
* @param {*} filterValue - The filter value to compare against
|
||||
* @param {Function} compareFn - The comparison function to apply
|
||||
* @returns {Boolean} - Returns true if the comparison succeeds, false otherwise
|
||||
*/
|
||||
const compareDates = (conversationValue, filterValue, compareFn) => {
|
||||
const conversationDate = coerceToDate(conversationValue);
|
||||
const filterDate = coerceToDate(filterValue);
|
||||
if (conversationDate === null || filterDate === null) return false;
|
||||
return compareFn(conversationDate, filterDate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a value matches a filter condition
|
||||
* @param {*} conversationValue - The value to check
|
||||
@@ -195,10 +210,10 @@ const matchesCondition = (conversationValue, filter) => {
|
||||
return false; // We already handled null/undefined above
|
||||
|
||||
case 'is_greater_than':
|
||||
return new Date(conversationValue) > new Date(filterValue);
|
||||
return compareDates(conversationValue, filterValue, (a, b) => a > b);
|
||||
|
||||
case 'is_less_than':
|
||||
return new Date(conversationValue) < new Date(filterValue);
|
||||
return compareDates(conversationValue, filterValue, (a, b) => a < b);
|
||||
|
||||
case 'days_before': {
|
||||
const today = new Date();
|
||||
@@ -347,6 +362,7 @@ export const matchesFilters = (conversation, filters) => {
|
||||
conversation,
|
||||
filters[0].attribute_key
|
||||
);
|
||||
|
||||
return matchesCondition(value, filters[0]);
|
||||
}
|
||||
|
||||
|
||||
+235
@@ -463,6 +463,241 @@ describe('filterHelpers', () => {
|
||||
expect(matchesFilters(conversation, filters)).toBe(true);
|
||||
});
|
||||
|
||||
// Test conversation with 10-digit timestamp (seconds) vs standard date filter
|
||||
it('should match conversation with 10-digit timestamp against date string filter', () => {
|
||||
const conversation = { created_at: 1647777600 }; // March 20, 2022 in seconds (10 digits)
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'created_at',
|
||||
filter_operator: 'is_greater_than',
|
||||
values: '2022-03-19', // Standard YYYY-MM-DD format
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
expect(matchesFilters(conversation, filters)).toBe(true);
|
||||
});
|
||||
|
||||
// Test conversation with 13-digit timestamp (milliseconds) vs standard date filter
|
||||
it('should match conversation with 13-digit timestamp against date string filter', () => {
|
||||
const conversation = { created_at: 1647777600000 }; // March 20, 2022 in milliseconds (13 digits)
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'created_at',
|
||||
filter_operator: 'is_greater_than',
|
||||
values: '2022-03-19', // Standard YYYY-MM-DD format
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
expect(matchesFilters(conversation, filters)).toBe(true);
|
||||
});
|
||||
|
||||
// Test conversation with string timestamp vs standard date filter
|
||||
it('should match conversation with string 10-digit timestamp against date string filter', () => {
|
||||
const conversation = { created_at: '1647777600' }; // March 20, 2022 as string (10 digits)
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'created_at',
|
||||
filter_operator: 'is_greater_than',
|
||||
values: '2022-03-19', // Standard YYYY-MM-DD format
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
expect(matchesFilters(conversation, filters)).toBe(true);
|
||||
});
|
||||
|
||||
// Test conversation with string 13-digit timestamp vs standard date filter
|
||||
it('should match conversation with string 13-digit timestamp against date string filter', () => {
|
||||
const conversation = { created_at: '1647777600000' }; // March 20, 2022 as string (13 digits)
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'created_at',
|
||||
filter_operator: 'is_greater_than',
|
||||
values: '2022-03-19', // Standard YYYY-MM-DD format
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
expect(matchesFilters(conversation, filters)).toBe(true);
|
||||
});
|
||||
|
||||
// Test conversation with mixed format vs standard date filter with time
|
||||
it('should match conversation with numeric timestamp against ISO date string filter', () => {
|
||||
const conversation = { created_at: 1647777600000 }; // March 20, 2022 12:00:00 GMT (numeric)
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'created_at',
|
||||
filter_operator: 'is_greater_than',
|
||||
values: '2022-03-19T10:30:00Z', // Standard ISO format from filter
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
expect(matchesFilters(conversation, filters)).toBe(true);
|
||||
});
|
||||
|
||||
// Test parseDate with date string without time (should default to 00:00:00)
|
||||
it('should match conversation with is_greater_than operator using date string without time', () => {
|
||||
const conversation = { created_at: 1647820800000 }; // March 21, 2022 00:00:00 GMT
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'created_at',
|
||||
filter_operator: 'is_greater_than',
|
||||
values: '2022-03-20', // March 20, 2022 (should become 00:00:00)
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
expect(matchesFilters(conversation, filters)).toBe(true);
|
||||
});
|
||||
|
||||
// Test parseDate with ISO date string
|
||||
it('should match conversation with is_greater_than operator using ISO date string', () => {
|
||||
const conversation = { created_at: 1647777600000 }; // March 20, 2022
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'created_at',
|
||||
filter_operator: 'is_greater_than',
|
||||
values: '2022-03-19T00:00:00.000Z', // March 19, 2022 ISO format
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
expect(matchesFilters(conversation, filters)).toBe(true);
|
||||
});
|
||||
|
||||
// Test parseDate with null/undefined values
|
||||
it('should handle null filter values in date comparison', () => {
|
||||
const conversation = { created_at: 1647777600000 }; // March 20, 2022
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'created_at',
|
||||
filter_operator: 'is_greater_than',
|
||||
values: null,
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
expect(matchesFilters(conversation, filters)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle undefined filter values in date comparison', () => {
|
||||
const conversation = { created_at: 1647777600000 }; // March 20, 2022
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'created_at',
|
||||
filter_operator: 'is_greater_than',
|
||||
values: undefined,
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
expect(matchesFilters(conversation, filters)).toBe(false);
|
||||
});
|
||||
|
||||
// Test parseDate with invalid date strings
|
||||
it('should handle invalid date strings in date comparison', () => {
|
||||
const conversation = { created_at: 1647777600000 }; // March 20, 2022
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'created_at',
|
||||
filter_operator: 'is_greater_than',
|
||||
values: 'invalid-date-string',
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
expect(matchesFilters(conversation, filters)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle non-date string values in date comparison', () => {
|
||||
const conversation = { created_at: 1647777600000 }; // March 20, 2022
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'created_at',
|
||||
filter_operator: 'is_greater_than',
|
||||
values: 'not-a-date',
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
expect(matchesFilters(conversation, filters)).toBe(false);
|
||||
});
|
||||
|
||||
// Test is_less_than with various date formats
|
||||
it('should match conversation with is_less_than operator using numeric timestamp', () => {
|
||||
const conversation = { created_at: 1647691200000 }; // March 19, 2022
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'created_at',
|
||||
filter_operator: 'is_less_than',
|
||||
values: 1647777600, // March 20, 2022 as 10-digit timestamp
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
expect(matchesFilters(conversation, filters)).toBe(true);
|
||||
});
|
||||
|
||||
it('should not match conversation with is_less_than operator when date is later', () => {
|
||||
const conversation = { created_at: 1647864000000 }; // March 21, 2022
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'created_at',
|
||||
filter_operator: 'is_less_than',
|
||||
values: '2022-03-20T12:00:00Z', // March 20, 2022 with time
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
expect(matchesFilters(conversation, filters)).toBe(false);
|
||||
});
|
||||
|
||||
// Edge case: Test with conversation having string timestamp
|
||||
it('should handle conversation with string timestamp value', () => {
|
||||
const conversation = { created_at: '1647777600000' }; // March 20, 2022 as string
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'created_at',
|
||||
filter_operator: 'is_greater_than',
|
||||
values: '2022-03-19',
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
expect(matchesFilters(conversation, filters)).toBe(true);
|
||||
});
|
||||
|
||||
// Edge case: Test with conversation having 10-digit timestamp
|
||||
it('should handle conversation with 10-digit timestamp value', () => {
|
||||
const conversation = { created_at: 1647777600 }; // March 20, 2022 as seconds (10 digits)
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'created_at',
|
||||
filter_operator: 'is_greater_than',
|
||||
values: '2022-03-19',
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
expect(matchesFilters(conversation, filters)).toBe(true);
|
||||
});
|
||||
|
||||
// Test date string with different time formats
|
||||
it('should handle date string with space-separated time', () => {
|
||||
const conversation = { created_at: 1647777600000 }; // March 20, 2022
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'created_at',
|
||||
filter_operator: 'is_greater_than',
|
||||
values: '2022-03-19 10:30:00', // Date with space-separated time
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
expect(matchesFilters(conversation, filters)).toBe(true);
|
||||
});
|
||||
|
||||
// Test parseDate with object input (should return null and fail comparison)
|
||||
it('should handle non-string, non-number filter values', () => {
|
||||
const conversation = { created_at: 1647777600000 }; // March 20, 2022
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'created_at',
|
||||
filter_operator: 'is_greater_than',
|
||||
values: { date: '2022-03-19' }, // Object instead of string/number
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
expect(matchesFilters(conversation, filters)).toBe(false);
|
||||
});
|
||||
|
||||
describe('days_before operator', () => {
|
||||
beforeEach(() => {
|
||||
// Set the date to March 25, 2022
|
||||
|
||||
@@ -204,6 +204,12 @@ export const mutations = {
|
||||
_state.allConversations.push(conversation);
|
||||
},
|
||||
|
||||
[types.DELETE_CONVERSATION](_state, conversationId) {
|
||||
_state.allConversations = _state.allConversations.filter(
|
||||
c => c.id !== conversationId
|
||||
);
|
||||
},
|
||||
|
||||
[types.UPDATE_CONVERSATION](_state, conversation) {
|
||||
const { allConversations } = _state;
|
||||
const index = allConversations.findIndex(c => c.id === conversation.id);
|
||||
|
||||
@@ -513,6 +513,28 @@ describe('#deleteMessage', () => {
|
||||
expect(commit.mock.calls).toEqual([]);
|
||||
});
|
||||
|
||||
describe('#deleteConversation', () => {
|
||||
it('send correct actions if API is success', async () => {
|
||||
axios.delete.mockResolvedValue({
|
||||
data: { id: 1 },
|
||||
});
|
||||
await actions.deleteConversation({ commit, dispatch }, 1);
|
||||
expect(commit.mock.calls).toEqual([[types.DELETE_CONVERSATION, 1]]);
|
||||
expect(dispatch.mock.calls).toEqual([
|
||||
['conversationStats/get', {}, { root: true }],
|
||||
]);
|
||||
});
|
||||
|
||||
it('send no actions if API is error', async () => {
|
||||
axios.delete.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await expect(
|
||||
actions.deleteConversation({ commit, dispatch }, 1)
|
||||
).rejects.toThrow(Error);
|
||||
expect(commit.mock.calls).toEqual([]);
|
||||
expect(dispatch.mock.calls).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#updateCustomAttributes', () => {
|
||||
it('update conversation custom attributes', async () => {
|
||||
axios.post.mockResolvedValue({
|
||||
|
||||
@@ -884,6 +884,17 @@ describe('#mutations', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#DELETE_CONVERSATION', () => {
|
||||
it('should delete a conversation', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1, messages: [] }],
|
||||
};
|
||||
|
||||
mutations[types.DELETE_CONVERSATION](state, 1);
|
||||
expect(state.allConversations).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_LIST_LOADING_STATUS', () => {
|
||||
it('should set listLoadingStatus to true', () => {
|
||||
const state = {
|
||||
|
||||
@@ -56,6 +56,7 @@ export default {
|
||||
SET_ALL_ATTACHMENTS: 'SET_ALL_ATTACHMENTS',
|
||||
ADD_CONVERSATION_ATTACHMENTS: 'ADD_CONVERSATION_ATTACHMENTS',
|
||||
DELETE_CONVERSATION_ATTACHMENTS: 'DELETE_CONVERSATION_ATTACHMENTS',
|
||||
DELETE_CONVERSATION: 'DELETE_CONVERSATION',
|
||||
|
||||
SET_CONVERSATION_CAN_REPLY: 'SET_CONVERSATION_CAN_REPLY',
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import HeaderActions from './HeaderActions.vue';
|
||||
import { computed } from 'vue';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
|
||||
const props = defineProps({
|
||||
avatarUrl: {
|
||||
@@ -21,6 +22,8 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
|
||||
const containerClasses = computed(() => [
|
||||
props.avatarUrl ? 'justify-between' : 'justify-end',
|
||||
]);
|
||||
@@ -47,8 +50,8 @@ const containerClasses = computed(() => [
|
||||
class="mt-4 text-2xl mb-1.5 font-medium text-n-slate-12"
|
||||
/>
|
||||
<p
|
||||
v-dompurify-html="introBody"
|
||||
class="text-lg leading-normal text-n-slate-11"
|
||||
v-dompurify-html="formatMessage(introBody)"
|
||||
class="text-lg leading-normal text-n-slate-11 [&_a]:underline"
|
||||
/>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user