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 { " /> -