diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb
index 8753918fc..e27869d82 100644
--- a/app/controllers/api/v1/accounts/conversations_controller.rb
+++ b/app/controllers/api/v1/accounts/conversations_controller.rb
@@ -124,6 +124,12 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
@conversation.save!
end
+ def destroy
+ authorize @conversation, :destroy?
+ ::DeleteObjectJob.perform_later(@conversation, Current.user, request.ip)
+ head :ok
+ end
+
private
def permitted_update_params
diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb
index 4d675593f..773126755 100644
--- a/app/controllers/api/v1/accounts_controller.rb
+++ b/app/controllers/api/v1/accounts_controller.rb
@@ -92,7 +92,7 @@ class Api::V1::AccountsController < Api::BaseController
end
def settings_params
- params.permit(:auto_resolve_after, :auto_resolve_message, :auto_resolve_ignore_waiting, :auto_resolve_label)
+ params.permit(:auto_resolve_after, :auto_resolve_message, :auto_resolve_ignore_waiting, :audio_transcriptions, :auto_resolve_label)
end
def check_signup_enabled
diff --git a/app/controllers/oauth_callback_controller.rb b/app/controllers/oauth_callback_controller.rb
index 4cb02d266..9aa73956a 100644
--- a/app/controllers/oauth_callback_controller.rb
+++ b/app/controllers/oauth_callback_controller.rb
@@ -71,6 +71,7 @@ class OauthCallbackController < ApplicationController
def create_channel_with_inbox
ActiveRecord::Base.transaction do
channel_email = Channel::Email.create!(email: users_data['email'], account: account)
+
account.inboxes.create!(
account: account,
channel: channel_email,
diff --git a/app/javascript/dashboard/api/inbox/conversation.js b/app/javascript/dashboard/api/inbox/conversation.js
index 1a87e6d96..0f539bfa9 100644
--- a/app/javascript/dashboard/api/inbox/conversation.js
+++ b/app/javascript/dashboard/api/inbox/conversation.js
@@ -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();
diff --git a/app/javascript/dashboard/components-next/Editor/Editor.vue b/app/javascript/dashboard/components-next/Editor/Editor.vue
index 6b5fefab9..510ae67e2 100644
--- a/app/javascript/dashboard/components-next/Editor/Editor.vue
+++ b/app/javascript/dashboard/components-next/Editor/Editor.vue
@@ -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"
diff --git a/app/javascript/dashboard/components-next/message/chips/Audio.vue b/app/javascript/dashboard/components-next/message/chips/Audio.vue
index 680584048..431058463 100644
--- a/app/javascript/dashboard/components-next/message/chips/Audio.vue
+++ b/app/javascript/dashboard/components-next/message/chips/Audio.vue
@@ -109,49 +109,58 @@ const downloadAudio = async () => {
-
-
- {{ formatTime(currentTime) }} / {{ formatTime(duration) }}
+
+
+
+ {{ formatTime(currentTime) }} / {{ formatTime(duration) }}
+
+
+
+
+
+
+
-
-
+
+
+ {{ attachment.transcribedText }}
-
-
-
diff --git a/app/javascript/dashboard/components/ChatList.vue b/app/javascript/dashboard/components/ChatList.vue
index 089d9e74f..098bc41c1 100644
--- a/app/javascript/dashboard/components/ChatList.vue
+++ b/app/javascript/dashboard/components/ChatList.vue
@@ -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) => {
+
diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue
index 698690dad..4840b459f 100644
--- a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue
@@ -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();
+ },
},
};
@@ -363,6 +368,7 @@ export default {
@mark-as-unread="markAsUnread"
@mark-as-read="markAsRead"
@assign-priority="assignPriority"
+ @delete-conversation="deleteConversation"
/>
diff --git a/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue b/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue
index fccff3457..6c788bbd2 100644
--- a/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue
@@ -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)"
/>
+
+
+
+
diff --git a/app/javascript/dashboard/constants/editor.js b/app/javascript/dashboard/constants/editor.js
index 157ec46ae..9a99516e8 100644
--- a/app/javascript/dashboard/constants/editor.js
+++ b/app/javascript/dashboard/constants/editor.js
@@ -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',
diff --git a/app/javascript/dashboard/helper/auditlogHelper.js b/app/javascript/dashboard/helper/auditlogHelper.js
index 6f5a4dede..706d09ba2 100644
--- a/app/javascript/dashboard/helper/auditlogHelper.js
+++ b/app/javascript/dashboard/helper/auditlogHelper.js
@@ -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,
diff --git a/app/javascript/dashboard/i18n/locale/en/auditLogs.json b/app/javascript/dashboard/i18n/locale/en/auditLogs.json
index 8194c667c..f85ad2a3e 100644
--- a/app/javascript/dashboard/i18n/locale/en/auditLogs.json
+++ b/app/javascript/dashboard/i18n/locale/en/auditLogs.json
@@ -69,6 +69,9 @@
},
"ACCOUNT": {
"EDIT": "{agentName} updated the account configuration (#{id})"
+ },
+ "CONVERSATION": {
+ "DELETE": "{agentName} deleted conversation #{id}"
}
}
}
diff --git a/app/javascript/dashboard/i18n/locale/en/conversation.json b/app/javascript/dashboard/i18n/locale/en/conversation.json
index 26ec6dc16..fdb7fc07a 100644
--- a/app/javascript/dashboard/i18n/locale/en/conversation.json
+++ b/app/javascript/dashboard/i18n/locale/en/conversation.json
@@ -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:",
diff --git a/app/javascript/dashboard/i18n/locale/en/generalSettings.json b/app/javascript/dashboard/i18n/locale/en/generalSettings.json
index d243c0583..c0c4d247d 100644
--- a/app/javascript/dashboard/i18n/locale/en/generalSettings.json
+++ b/app/javascript/dashboard/i18n/locale/en/generalSettings.json
@@ -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."
diff --git a/app/javascript/dashboard/modules/widget-preview/components/WidgetHead.vue b/app/javascript/dashboard/modules/widget-preview/components/WidgetHead.vue
index 4881c2a0a..5de9ca069 100644
--- a/app/javascript/dashboard/modules/widget-preview/components/WidgetHead.vue
+++ b/app/javascript/dashboard/modules/widget-preview/components/WidgetHead.vue
@@ -1,5 +1,6 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue
index d9551c427..a3bb9f3f4 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue
@@ -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 {
"
/>
-