Compare commits
47
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d44509a413 | ||
|
|
fe5670832a | ||
|
|
774202e462 | ||
|
|
bc6420019f | ||
|
|
37979b89a6 | ||
|
|
f82ec3b885 | ||
|
|
32c25047c4 | ||
|
|
7f8d718da3 | ||
|
|
3489783cb8 | ||
|
|
d57d302446 | ||
|
|
53d68868c6 | ||
|
|
b61ad6e41a | ||
|
|
41c5e7d3f1 | ||
|
|
57760bba20 | ||
|
|
abc511d00f | ||
|
|
a48f98de9d | ||
|
|
7c2353c7d9 | ||
|
|
b221987b2b | ||
|
|
c80de24ac1 | ||
|
|
dadd572f9d | ||
|
|
776579ba5b | ||
|
|
4aa4e2549f | ||
|
|
eb6de74269 | ||
|
|
d19a9c38d7 | ||
|
|
c63a6ed8ec | ||
|
|
429d281501 | ||
|
|
26dd040123 | ||
|
|
6571baf211 | ||
|
|
44227de97e | ||
|
|
77b718c22c | ||
|
|
04b67eb431 | ||
|
|
a92d7fd7d5 | ||
|
|
e9e515c436 | ||
|
|
bb947ca50d | ||
|
|
00a5ea5178 | ||
|
|
8473e72a7e | ||
|
|
292435e0e7 | ||
|
|
43adbe5743 | ||
|
|
3da2ce6e6a | ||
|
|
383bccbee8 | ||
|
|
be2e75ffb5 | ||
|
|
88cb3fc76a | ||
|
|
e09fceffc9 | ||
|
|
4b511da098 | ||
|
|
2d8a759655 | ||
|
|
c1cc921425 | ||
|
|
dcbb045bcd |
@@ -25,13 +25,5 @@ jobs:
|
||||
with:
|
||||
issue-inactive-days: '30'
|
||||
issue-lock-reason: 'resolved'
|
||||
issue-comment: >
|
||||
This issue has been automatically locked since there
|
||||
has not been any recent activity after it was closed.
|
||||
Please open a new issue for related bugs.
|
||||
pr-inactive-days: '30'
|
||||
pr-lock-reason: 'resolved'
|
||||
pr-comment: >
|
||||
This pull request has been automatically locked since there
|
||||
has not been any recent activity after it was closed.
|
||||
Please open a new issue for related bugs.
|
||||
|
||||
@@ -116,7 +116,7 @@ gem 'sentry-ruby', require: false
|
||||
gem 'sentry-sidekiq', '>= 5.19.0', require: false
|
||||
|
||||
##-- background job processing --##
|
||||
gem 'sidekiq', '>= 7.3.0'
|
||||
gem 'sidekiq', '>= 7.3.1'
|
||||
# We want cron jobs
|
||||
gem 'sidekiq-cron', '>= 1.12.0'
|
||||
|
||||
@@ -228,7 +228,7 @@ group :development, :test do
|
||||
gem 'mock_redis'
|
||||
gem 'pry-rails'
|
||||
gem 'rspec_junit_formatter'
|
||||
gem 'rspec-rails', '>= 6.1.3'
|
||||
gem 'rspec-rails', '>= 6.1.4'
|
||||
gem 'rubocop', require: false
|
||||
gem 'rubocop-performance', require: false
|
||||
gem 'rubocop-rails', require: false
|
||||
|
||||
+12
-12
@@ -169,7 +169,7 @@ GEM
|
||||
climate_control (1.2.0)
|
||||
coderay (1.1.3)
|
||||
commonmarker (0.23.10)
|
||||
concurrent-ruby (1.3.3)
|
||||
concurrent-ruby (1.3.4)
|
||||
connection_pool (2.4.1)
|
||||
crack (1.0.0)
|
||||
bigdecimal
|
||||
@@ -230,7 +230,7 @@ GEM
|
||||
ruby2_keywords
|
||||
email_reply_trimmer (0.1.13)
|
||||
erubi (1.13.0)
|
||||
et-orbi (1.2.7)
|
||||
et-orbi (1.2.11)
|
||||
tzinfo
|
||||
execjs (2.8.1)
|
||||
facebook-messenger (2.0.1)
|
||||
@@ -268,8 +268,8 @@ GEM
|
||||
rake
|
||||
flag_shih_tzu (0.3.23)
|
||||
foreman (0.87.2)
|
||||
fugit (1.9.0)
|
||||
et-orbi (~> 1, >= 1.2.7)
|
||||
fugit (1.11.1)
|
||||
et-orbi (~> 1, >= 1.2.11)
|
||||
raabro (~> 1.4)
|
||||
gapic-common (0.18.0)
|
||||
faraday (>= 1.9, < 3.a)
|
||||
@@ -467,7 +467,7 @@ GEM
|
||||
mini_magick (4.12.0)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.7)
|
||||
minitest (5.24.1)
|
||||
minitest (5.25.1)
|
||||
mock_redis (0.36.0)
|
||||
ruby2_keywords
|
||||
msgpack (1.7.0)
|
||||
@@ -480,7 +480,7 @@ GEM
|
||||
uri
|
||||
net-http-persistent (4.0.2)
|
||||
connection_pool (~> 2.2)
|
||||
net-imap (0.4.12)
|
||||
net-imap (0.4.14)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
@@ -634,7 +634,7 @@ GEM
|
||||
retriable (3.1.2)
|
||||
reverse_markdown (2.1.1)
|
||||
nokogiri
|
||||
rexml (3.3.4)
|
||||
rexml (3.3.6)
|
||||
strscan
|
||||
rspec-core (3.13.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
@@ -644,7 +644,7 @@ GEM
|
||||
rspec-mocks (3.13.1)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-rails (6.1.3)
|
||||
rspec-rails (6.1.4)
|
||||
actionpack (>= 6.1)
|
||||
activesupport (>= 6.1)
|
||||
railties (>= 6.1)
|
||||
@@ -722,7 +722,7 @@ GEM
|
||||
sexp_processor (4.17.0)
|
||||
shoulda-matchers (5.3.0)
|
||||
activesupport (>= 5.2.0)
|
||||
sidekiq (7.3.0)
|
||||
sidekiq (7.3.1)
|
||||
concurrent-ruby (< 2)
|
||||
connection_pool (>= 2.3.0)
|
||||
logger
|
||||
@@ -796,7 +796,7 @@ GEM
|
||||
uniform_notifier (1.16.0)
|
||||
uri (0.13.0)
|
||||
uri_template (0.7.0)
|
||||
valid_email2 (4.0.6)
|
||||
valid_email2 (5.2.6)
|
||||
activemodel (>= 3.2)
|
||||
mail (~> 2.5)
|
||||
version_gem (1.1.4)
|
||||
@@ -930,7 +930,7 @@ DEPENDENCIES
|
||||
responders (>= 3.1.1)
|
||||
rest-client
|
||||
reverse_markdown
|
||||
rspec-rails (>= 6.1.3)
|
||||
rspec-rails (>= 6.1.4)
|
||||
rspec_junit_formatter
|
||||
rubocop
|
||||
rubocop-performance
|
||||
@@ -943,7 +943,7 @@ DEPENDENCIES
|
||||
sentry-ruby
|
||||
sentry-sidekiq (>= 5.19.0)
|
||||
shoulda-matchers
|
||||
sidekiq (>= 7.3.0)
|
||||
sidekiq (>= 7.3.1)
|
||||
sidekiq-cron (>= 1.12.0)
|
||||
simplecov (= 0.17.1)
|
||||
slack-ruby-client (~> 2.2.0)
|
||||
|
||||
@@ -32,11 +32,13 @@ class AccountBuilder
|
||||
end
|
||||
|
||||
def validate_email
|
||||
raise InvalidEmail.new({ domain_blocked: domain_blocked }) if domain_blocked?
|
||||
|
||||
address = ValidEmail2::Address.new(@email)
|
||||
if address.valid? # && !address.disposable?
|
||||
if address.valid? && !address.disposable?
|
||||
true
|
||||
else
|
||||
raise InvalidEmail.new(valid: address.valid?)
|
||||
raise InvalidEmail.new({ valid: address.valid?, disposable: address.disposable? })
|
||||
end
|
||||
end
|
||||
|
||||
@@ -79,4 +81,21 @@ class AccountBuilder
|
||||
@user.confirm if @confirmed
|
||||
@user.save!
|
||||
end
|
||||
|
||||
def domain_blocked?
|
||||
domain = @email.split('@').last
|
||||
|
||||
blocked_domains.each do |blocked_domain|
|
||||
return true if domain.match?(blocked_domain)
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def blocked_domains
|
||||
domains = GlobalConfigService.load('BLOCKED_EMAIL_DOMAINS', '')
|
||||
domains.split("\n").map(&:strip) if domains.present?
|
||||
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -24,7 +24,7 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||
|
||||
def update
|
||||
@agent.update!(agent_params.slice(:name).compact)
|
||||
@agent.current_account_user.update!(agent_params.slice(:role, :availability, :auto_offline).compact)
|
||||
@agent.current_account_user.update!(agent_params.slice(*account_user_attributes).compact)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@@ -67,8 +67,16 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||
@agent = agents.find(params[:id])
|
||||
end
|
||||
|
||||
def account_user_attributes
|
||||
[:role, :availability, :auto_offline]
|
||||
end
|
||||
|
||||
def allowed_agent_params
|
||||
[:name, :email, :name, :role, :availability, :auto_offline]
|
||||
end
|
||||
|
||||
def agent_params
|
||||
params.require(:agent).permit(:name, :email, :name, :role, :availability, :auto_offline)
|
||||
params.require(:agent).permit(allowed_agent_params)
|
||||
end
|
||||
|
||||
def new_agent_params
|
||||
@@ -101,3 +109,5 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||
DeleteObjectJob.perform_later(agent) if agent.reload.account_users.blank?
|
||||
end
|
||||
end
|
||||
|
||||
Api::V1::Accounts::AgentsController.prepend_mod_with('Api::V1::Accounts::AgentsController')
|
||||
|
||||
@@ -11,7 +11,17 @@ class Api::V1::Accounts::Integrations::HooksController < Api::V1::Accounts::Base
|
||||
end
|
||||
|
||||
def process_event
|
||||
render json: { message: @hook.process_event(params[:event]) }
|
||||
response = @hook.process_event(params[:event])
|
||||
|
||||
# for cases like an invalid event, or when conversation does not have enough messages
|
||||
# for a label suggestion, the response is nil
|
||||
if response.nil?
|
||||
render json: { message: nil }
|
||||
elsif response[:error]
|
||||
render json: { error: response[:error] }, status: :unprocessable_entity
|
||||
else
|
||||
render json: { message: response[:message] }
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
|
||||
@@ -4,6 +4,7 @@ import { mapGetters } from 'vuex';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
import { useFilter } from 'shared/composables/useFilter';
|
||||
import VirtualList from 'vue-virtual-scroll-list';
|
||||
|
||||
import ChatListHeader from './ChatListHeader.vue';
|
||||
@@ -16,7 +17,6 @@ import filterQueryGenerator from '../helper/filterQueryGenerator.js';
|
||||
import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews.vue';
|
||||
import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue';
|
||||
import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue';
|
||||
import filterMixin from 'shared/mixins/filterMixin';
|
||||
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||
import countries from 'shared/constants/countries';
|
||||
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
|
||||
@@ -41,7 +41,6 @@ export default {
|
||||
IntersectionObserver,
|
||||
VirtualList,
|
||||
},
|
||||
mixins: [filterMixin],
|
||||
provide() {
|
||||
return {
|
||||
// Actions to be performed on virtual list item and context menu.
|
||||
@@ -91,6 +90,15 @@ export default {
|
||||
|
||||
const conversationListRef = ref(null);
|
||||
|
||||
const {
|
||||
setFilterAttributes,
|
||||
initializeStatusAndAssigneeFilterToModal,
|
||||
initializeInboxTeamAndLabelFilterToModal,
|
||||
} = useFilter({
|
||||
filteri18nKey: 'FILTER',
|
||||
attributeModel: 'conversation_attribute',
|
||||
});
|
||||
|
||||
const getKeyboardListenerParams = () => {
|
||||
const allConversations = conversationListRef.value.querySelectorAll(
|
||||
'div.conversations-list div.conversation'
|
||||
@@ -141,11 +149,14 @@ export default {
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
useKeyboardEvents(keyboardEvents, conversationListRef);
|
||||
useKeyboardEvents(keyboardEvents);
|
||||
|
||||
return {
|
||||
uiSettings,
|
||||
conversationListRef,
|
||||
setFilterAttributes,
|
||||
initializeStatusAndAssigneeFilterToModal,
|
||||
initializeInboxTeamAndLabelFilterToModal,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
@@ -860,6 +871,25 @@ export default {
|
||||
onContextMenuToggle(state) {
|
||||
this.isContextMenuOpen = state;
|
||||
},
|
||||
initializeExistingFilterToModal() {
|
||||
const statusFilter = this.initializeStatusAndAssigneeFilterToModal(
|
||||
this.activeStatus,
|
||||
this.currentUserDetails,
|
||||
this.activeAssigneeTab
|
||||
);
|
||||
if (statusFilter) {
|
||||
this.appliedFilter.push(statusFilter);
|
||||
}
|
||||
|
||||
const otherFilters = this.initializeInboxTeamAndLabelFilterToModal(
|
||||
this.conversationInbox,
|
||||
this.inbox,
|
||||
this.teamId,
|
||||
this.activeTeam,
|
||||
this.label
|
||||
);
|
||||
this.appliedFilter.push(...otherFilters);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -13,13 +13,12 @@ import wootConstants from 'dashboard/constants/globals';
|
||||
import {
|
||||
CMD_REOPEN_CONVERSATION,
|
||||
CMD_RESOLVE_CONVERSATION,
|
||||
} from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
|
||||
} from 'dashboard/helper/commandbar/events';
|
||||
|
||||
const store = useStore();
|
||||
const getters = useStoreGetters();
|
||||
const { t } = useI18n();
|
||||
|
||||
const resolveActionsRef = ref(null);
|
||||
const arrowDownButtonRef = ref(null);
|
||||
const isLoading = ref(false);
|
||||
|
||||
@@ -131,17 +130,14 @@ const keyboardEvents = {
|
||||
},
|
||||
};
|
||||
|
||||
useKeyboardEvents(keyboardEvents, resolveActionsRef);
|
||||
useKeyboardEvents(keyboardEvents);
|
||||
|
||||
useEmitter(CMD_REOPEN_CONVERSATION, onCmdOpenConversation);
|
||||
useEmitter(CMD_RESOLVE_CONVERSATION, onCmdResolveConversation);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="resolveActionsRef"
|
||||
class="relative flex items-center justify-end resolve-actions"
|
||||
>
|
||||
<div class="relative flex items-center justify-end resolve-actions">
|
||||
<div class="button-group">
|
||||
<woot-button
|
||||
v-if="isOpen"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script>
|
||||
import { ref } from 'vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { getSidebarItems } from './config/default-sidebar';
|
||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
@@ -8,7 +7,10 @@ import { useRoute, useRouter } from 'dashboard/composables/route';
|
||||
import PrimarySidebar from './sidebarComponents/Primary.vue';
|
||||
import SecondarySidebar from './sidebarComponents/Secondary.vue';
|
||||
import { routesWithPermissions } from '../../routes';
|
||||
import { hasPermissions } from '../../helper/permissionsHelper';
|
||||
import {
|
||||
getUserPermissions,
|
||||
hasPermissions,
|
||||
} from '../../helper/permissionsHelper';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -26,7 +28,6 @@ export default {
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const sidebarRef = ref(null);
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
@@ -64,11 +65,10 @@ export default {
|
||||
action: () => navigateToRoute('agent_list'),
|
||||
},
|
||||
};
|
||||
useKeyboardEvents(keyboardEvents, sidebarRef);
|
||||
useKeyboardEvents(keyboardEvents);
|
||||
|
||||
return {
|
||||
toggleKeyShortcutModal,
|
||||
sidebarRef,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
@@ -113,7 +113,10 @@ export default {
|
||||
return getSidebarItems(this.accountId);
|
||||
},
|
||||
primaryMenuItems() {
|
||||
const userPermissions = this.currentUser.permissions;
|
||||
const userPermissions = getUserPermissions(
|
||||
this.currentUser,
|
||||
this.accountId
|
||||
);
|
||||
const menuItems = this.sideMenuConfig.primaryMenu;
|
||||
return menuItems.filter(menuItem => {
|
||||
const isAvailableForTheUser = hasPermissions(
|
||||
@@ -195,7 +198,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside ref="sidebarRef" class="flex h-full">
|
||||
<aside class="flex h-full">
|
||||
<PrimarySidebar
|
||||
:logo-source="globalConfig.logoThumbnail"
|
||||
:installation-name="globalConfig.installationName"
|
||||
|
||||
@@ -4,7 +4,10 @@ import SecondaryNavItem from './SecondaryNavItem.vue';
|
||||
import AccountContext from './AccountContext.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { FEATURE_FLAGS } from '../../../featureFlags';
|
||||
import { hasPermissions } from '../../../helper/permissionsHelper';
|
||||
import {
|
||||
getUserPermissions,
|
||||
hasPermissions,
|
||||
} from '../../../helper/permissionsHelper';
|
||||
import { routesWithPermissions } from '../../../routes';
|
||||
|
||||
export default {
|
||||
@@ -59,7 +62,10 @@ export default {
|
||||
accessibleMenuItems() {
|
||||
const menuItemsFilteredByPermissions = this.menuConfig.menuItems.filter(
|
||||
menuItem => {
|
||||
const { permissions: userPermissions = [] } = this.currentUser;
|
||||
const userPermissions = getUserPermissions(
|
||||
this.currentUser,
|
||||
this.accountId
|
||||
);
|
||||
return hasPermissions(
|
||||
routesWithPermissions[menuItem.toStateName],
|
||||
userPermissions
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<script setup>
|
||||
import { useStoreGetters } from 'dashboard/composables/store';
|
||||
import { computed } from 'vue';
|
||||
import { hasPermissions } from '../helper/permissionsHelper';
|
||||
import {
|
||||
getUserPermissions,
|
||||
hasPermissions,
|
||||
} from '../helper/permissionsHelper';
|
||||
|
||||
const props = defineProps({
|
||||
permissions: {
|
||||
type: Array,
|
||||
@@ -10,12 +14,17 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const getters = useStoreGetters();
|
||||
const user = getters.getCurrentUser.value;
|
||||
const hasPermission = computed(() =>
|
||||
hasPermissions(props.permissions, user.permissions)
|
||||
);
|
||||
const user = computed(() => getters.getCurrentUser.value);
|
||||
const accountId = computed(() => getters.getCurrentAccountId.value);
|
||||
const userPermissions = computed(() => {
|
||||
return getUserPermissions(user.value, accountId.value);
|
||||
});
|
||||
const hasPermission = computed(() => {
|
||||
return hasPermissions(props.permissions, userPermissions.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable vue/no-root-v-if -->
|
||||
<template>
|
||||
<div v-if="hasPermission">
|
||||
<slot />
|
||||
|
||||
@@ -4,10 +4,10 @@ import { mapGetters } from 'vuex';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
import { useAI } from 'dashboard/composables/useAI';
|
||||
import AICTAModal from './AICTAModal.vue';
|
||||
import AIAssistanceModal from './AIAssistanceModal.vue';
|
||||
import aiMixin from 'dashboard/mixins/aiMixin';
|
||||
import { CMD_AI_ASSIST } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
|
||||
import { CMD_AI_ASSIST } from 'dashboard/helper/commandbar/events';
|
||||
import AIAssistanceCTAButton from './AIAssistanceCTAButton.vue';
|
||||
|
||||
export default {
|
||||
@@ -16,17 +16,17 @@ export default {
|
||||
AICTAModal,
|
||||
AIAssistanceCTAButton,
|
||||
},
|
||||
mixins: [aiMixin],
|
||||
setup(props, { emit }) {
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
|
||||
const { isAIIntegrationEnabled, draftMessage, recordAnalytics } = useAI();
|
||||
|
||||
const { isAdmin } = useAdmin();
|
||||
|
||||
const aiAssistanceButtonRef = ref(null);
|
||||
const initialMessage = ref('');
|
||||
|
||||
const initializeMessage = draftMessage => {
|
||||
initialMessage.value = draftMessage;
|
||||
const initializeMessage = draftMsg => {
|
||||
initialMessage.value = draftMsg;
|
||||
};
|
||||
const keyboardEvents = {
|
||||
'$mod+KeyZ': {
|
||||
@@ -39,15 +39,17 @@ export default {
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
useKeyboardEvents(keyboardEvents, aiAssistanceButtonRef);
|
||||
useKeyboardEvents(keyboardEvents);
|
||||
|
||||
return {
|
||||
uiSettings,
|
||||
updateUISettings,
|
||||
isAdmin,
|
||||
aiAssistanceButtonRef,
|
||||
initialMessage,
|
||||
initializeMessage,
|
||||
recordAnalytics,
|
||||
isAIIntegrationEnabled,
|
||||
draftMessage,
|
||||
};
|
||||
},
|
||||
data: () => ({
|
||||
@@ -118,7 +120,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="aiAssistanceButtonRef">
|
||||
<div>
|
||||
<div v-if="isAIIntegrationEnabled" class="relative">
|
||||
<AIAssistanceCTAButton
|
||||
v-if="shouldShowAIAssistCTAButton"
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
<script>
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import { useAI } from 'dashboard/composables/useAI';
|
||||
import AILoader from './AILoader.vue';
|
||||
import aiMixin from 'dashboard/mixins/aiMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AILoader,
|
||||
},
|
||||
mixins: [aiMixin, messageFormatterMixin],
|
||||
props: {
|
||||
aiOption: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
setup() {
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
const { draftMessage, processEvent, recordAnalytics } = useAI();
|
||||
return { draftMessage, processEvent, recordAnalytics, formatMessage };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
generatedContent: '',
|
||||
|
||||
@@ -3,16 +3,16 @@ import { useVuelidate } from '@vuelidate/core';
|
||||
import { required } from '@vuelidate/validators';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import aiMixin from 'dashboard/mixins/aiMixin';
|
||||
import { useAI } from 'dashboard/composables/useAI';
|
||||
import { OPEN_AI_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
|
||||
export default {
|
||||
mixins: [aiMixin],
|
||||
setup() {
|
||||
const { updateUISettings } = useUISettings();
|
||||
const { recordAnalytics } = useAI();
|
||||
const v$ = useVuelidate();
|
||||
|
||||
return { updateUISettings, v$ };
|
||||
return { updateUISettings, v$, recordAnalytics };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
|
||||
@@ -16,8 +16,6 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['chatTabChange']);
|
||||
|
||||
const chatTypeTabsRef = ref(null);
|
||||
|
||||
const activeTabIndex = computed(() => {
|
||||
return props.items.findIndex(item => item.key === props.activeTab);
|
||||
});
|
||||
@@ -39,24 +37,23 @@ const keyboardEvents = {
|
||||
},
|
||||
},
|
||||
};
|
||||
useKeyboardEvents(keyboardEvents, chatTypeTabsRef);
|
||||
|
||||
useKeyboardEvents(keyboardEvents);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="chatTypeTabsRef">
|
||||
<woot-tabs
|
||||
:index="activeTabIndex"
|
||||
class="tab--chat-type py-0 px-4 w-full"
|
||||
@change="onTabChange"
|
||||
>
|
||||
<woot-tabs-item
|
||||
v-for="item in items"
|
||||
:key="item.key"
|
||||
:name="item.name"
|
||||
:count="item.count"
|
||||
/>
|
||||
</woot-tabs>
|
||||
</div>
|
||||
<woot-tabs
|
||||
:index="activeTabIndex"
|
||||
class="w-full px-4 py-0 tab--chat-type"
|
||||
@change="onTabChange"
|
||||
>
|
||||
<woot-tabs-item
|
||||
v-for="item in items"
|
||||
:key="item.key"
|
||||
:name="item.name"
|
||||
:count="item.count"
|
||||
/>
|
||||
</woot-tabs>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -18,8 +18,6 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['add', 'remove']);
|
||||
|
||||
const labelSelectorWrapRef = ref(null);
|
||||
|
||||
const { isAdmin } = useAdmin();
|
||||
|
||||
const showSearchDropdownLabel = ref(false);
|
||||
@@ -57,15 +55,11 @@ const keyboardEvents = {
|
||||
},
|
||||
};
|
||||
|
||||
useKeyboardEvents(keyboardEvents, labelSelectorWrapRef);
|
||||
useKeyboardEvents(keyboardEvents);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="labelSelectorWrapRef"
|
||||
v-on-clickaway="closeDropdownLabel"
|
||||
class="relative leading-6"
|
||||
>
|
||||
<div v-on-clickaway="closeDropdownLabel" class="relative leading-6">
|
||||
<AddLabel @add="toggleLabels" />
|
||||
<woot-label
|
||||
v-for="label in savedLabels"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script>
|
||||
import { ref, watchEffect, computed } from 'vue';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
import FileUpload from 'vue-upload-component';
|
||||
@@ -117,15 +116,13 @@ export default {
|
||||
const { setSignatureFlagForInbox, fetchSignatureFlagFromUISettings } =
|
||||
useUISettings();
|
||||
|
||||
const uploadRef = ref(null);
|
||||
// TODO: This is really hacky, we need to replace the file picker component with
|
||||
// a custom one, where the logic and the component markup is isolated.
|
||||
// Once we have the custom component, we can remove the hacky logic below.
|
||||
const uploadRefElem = computed(() => uploadRef.value?.$el);
|
||||
|
||||
const keyboardEvents = {
|
||||
'Alt+KeyA': {
|
||||
action: () => {
|
||||
// TODO: This is really hacky, we need to replace the file picker component with
|
||||
// a custom one, where the logic and the component markup is isolated.
|
||||
// Once we have the custom component, we can remove the hacky logic below.
|
||||
|
||||
const uploadTriggerButton = document.querySelector(
|
||||
'#conversationAttachment'
|
||||
);
|
||||
@@ -135,14 +132,11 @@ export default {
|
||||
},
|
||||
};
|
||||
|
||||
watchEffect(() => {
|
||||
useKeyboardEvents(keyboardEvents, uploadRefElem);
|
||||
});
|
||||
useKeyboardEvents(keyboardEvents);
|
||||
|
||||
return {
|
||||
setSignatureFlagForInbox,
|
||||
fetchSignatureFlagFromUISettings,
|
||||
uploadRef,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -267,7 +261,6 @@ export default {
|
||||
@click="toggleEmojiPicker"
|
||||
/>
|
||||
<FileUpload
|
||||
ref="uploadRef"
|
||||
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_ATTACH_ICON')"
|
||||
input-id="conversationAttachment"
|
||||
:size="4096 * 4096"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script>
|
||||
import { ref } from 'vue';
|
||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
import { REPLY_EDITOR_MODES, CHAR_LENGTH_WARNING } from './constants';
|
||||
export default {
|
||||
@@ -23,8 +22,6 @@ export default {
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const replyTopRef = ref(null);
|
||||
|
||||
const setReplyMode = mode => {
|
||||
emit('setReplyMode', mode);
|
||||
};
|
||||
@@ -44,12 +41,11 @@ export default {
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
useKeyboardEvents(keyboardEvents, replyTopRef);
|
||||
useKeyboardEvents(keyboardEvents);
|
||||
|
||||
return {
|
||||
handleReplyClick,
|
||||
handleNoteClick,
|
||||
replyTopRef,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -76,10 +72,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="replyTopRef"
|
||||
class="flex justify-between bg-black-50 dark:bg-slate-800"
|
||||
>
|
||||
<div class="flex justify-between bg-black-50 dark:bg-slate-800">
|
||||
<div class="button-group">
|
||||
<woot-button
|
||||
variant="clear"
|
||||
|
||||
+15
-3
@@ -5,7 +5,7 @@ import languages from './advancedFilterItems/languages';
|
||||
import countries from 'shared/constants/countries.js';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { filterAttributeGroups } from './advancedFilterItems';
|
||||
import filterMixin from 'shared/mixins/filterMixin';
|
||||
import { useFilter } from 'shared/composables/useFilter';
|
||||
import * as OPERATORS from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js';
|
||||
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||
import { validateConversationOrContactFilters } from 'dashboard/helper/validations.js';
|
||||
@@ -14,7 +14,6 @@ export default {
|
||||
components: {
|
||||
FilterInputBox,
|
||||
},
|
||||
mixins: [filterMixin],
|
||||
props: {
|
||||
onClose: {
|
||||
type: Function,
|
||||
@@ -37,6 +36,15 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { setFilterAttributes } = useFilter({
|
||||
filteri18nKey: 'FILTER',
|
||||
attributeModel: 'conversation_attribute',
|
||||
});
|
||||
return {
|
||||
setFilterAttributes,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: true,
|
||||
@@ -67,7 +75,11 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setFilterAttributes();
|
||||
const { filterGroups, filterTypes } = this.setFilterAttributes();
|
||||
|
||||
this.filterTypes = [...this.filterTypes, ...filterTypes];
|
||||
this.filterGroups = filterGroups;
|
||||
|
||||
this.$store.dispatch('campaigns/get');
|
||||
if (this.getAppliedConversationFilters.length) {
|
||||
this.appliedFilters = [];
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<script>
|
||||
import { ref } from 'vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
import agentMixin from '../../../mixins/agentMixin.js';
|
||||
import BackButton from '../BackButton.vue';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
import InboxName from '../InboxName.vue';
|
||||
@@ -24,7 +22,7 @@ export default {
|
||||
SLACardLabel,
|
||||
Linear,
|
||||
},
|
||||
mixins: [inboxMixin, agentMixin],
|
||||
mixins: [inboxMixin],
|
||||
props: {
|
||||
chat: {
|
||||
type: Object,
|
||||
@@ -44,18 +42,12 @@ export default {
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const conversationHeaderActionsRef = ref(null);
|
||||
|
||||
const keyboardEvents = {
|
||||
'Alt+KeyO': {
|
||||
action: () => emit('contactPanelToggle'),
|
||||
},
|
||||
};
|
||||
useKeyboardEvents(keyboardEvents, conversationHeaderActionsRef);
|
||||
|
||||
return {
|
||||
conversationHeaderActionsRef,
|
||||
};
|
||||
useKeyboardEvents(keyboardEvents);
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
@@ -183,7 +175,6 @@ export default {
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref="conversationHeaderActionsRef"
|
||||
class="flex items-center gap-2 overflow-hidden text-xs conversation--header--actions text-ellipsis whitespace-nowrap"
|
||||
>
|
||||
<InboxName v-if="hasMultipleInboxes" :inbox="inbox" />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ref } from 'vue';
|
||||
// composable
|
||||
import { useConfig } from 'dashboard/composables/useConfig';
|
||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
import { useAI } from 'dashboard/composables/useAI';
|
||||
|
||||
// components
|
||||
import ReplyBox from './ReplyBox.vue';
|
||||
@@ -15,7 +16,6 @@ import { mapGetters } from 'vuex';
|
||||
|
||||
// mixins
|
||||
import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
|
||||
import aiMixin from 'dashboard/mixins/aiMixin';
|
||||
|
||||
// utils
|
||||
import { getTypingUsersText } from '../../../helper/commons';
|
||||
@@ -40,7 +40,7 @@ export default {
|
||||
Banner,
|
||||
ConversationLabelSuggestion,
|
||||
},
|
||||
mixins: [inboxMixin, aiMixin],
|
||||
mixins: [inboxMixin],
|
||||
props: {
|
||||
isContactPanelOpen: {
|
||||
type: Boolean,
|
||||
@@ -52,7 +52,6 @@ export default {
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const conversationFooterRef = ref(null);
|
||||
const isPopOutReplyBox = ref(false);
|
||||
const { isEnterprise } = useConfig();
|
||||
|
||||
@@ -70,14 +69,24 @@ export default {
|
||||
},
|
||||
};
|
||||
|
||||
useKeyboardEvents(keyboardEvents, conversationFooterRef);
|
||||
useKeyboardEvents(keyboardEvents);
|
||||
|
||||
const {
|
||||
isAIIntegrationEnabled,
|
||||
isLabelSuggestionFeatureEnabled,
|
||||
fetchIntegrationsIfRequired,
|
||||
fetchLabelSuggestions,
|
||||
} = useAI();
|
||||
|
||||
return {
|
||||
isEnterprise,
|
||||
conversationFooterRef,
|
||||
isPopOutReplyBox,
|
||||
closePopOutReplyBox,
|
||||
showPopOutReplyBox,
|
||||
isAIIntegrationEnabled,
|
||||
isLabelSuggestionFeatureEnabled,
|
||||
fetchIntegrationsIfRequired,
|
||||
fetchLabelSuggestions,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
@@ -518,7 +527,6 @@ export default {
|
||||
/>
|
||||
</ul>
|
||||
<div
|
||||
ref="conversationFooterRef"
|
||||
class="conversation-footer"
|
||||
:class="{ 'modal-mask': isPopOutReplyBox }"
|
||||
>
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
CMD_MUTE_CONVERSATION,
|
||||
CMD_SEND_TRANSCRIPT,
|
||||
CMD_UNMUTE_CONVERSATION,
|
||||
} from '../../../routes/dashboard/commands/commandBarBusEvents';
|
||||
} from 'dashboard/helper/commandbar/events';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -40,7 +40,6 @@ const onSelect = () => {
|
||||
};
|
||||
|
||||
useKeyboardNavigableList({
|
||||
elementRef: tagAgentsRef,
|
||||
items,
|
||||
onSelect,
|
||||
adjustScroll,
|
||||
|
||||
@@ -35,7 +35,6 @@ const ALLOWED_FILE_TYPES = {
|
||||
const MAX_ZOOM_LEVEL = 2;
|
||||
const MIN_ZOOM_LEVEL = 1;
|
||||
|
||||
const galleryViewRef = ref(null);
|
||||
const zoomScale = ref(1);
|
||||
const activeAttachment = ref({});
|
||||
const activeFileType = ref('');
|
||||
@@ -202,7 +201,7 @@ const keyboardEvents = {
|
||||
},
|
||||
},
|
||||
};
|
||||
useKeyboardEvents(keyboardEvents, galleryViewRef);
|
||||
useKeyboardEvents(keyboardEvents);
|
||||
|
||||
onMounted(() => {
|
||||
setImageAndVideoSrc(props.attachment);
|
||||
@@ -218,7 +217,6 @@ onMounted(() => {
|
||||
:on-close="onClose"
|
||||
>
|
||||
<div
|
||||
ref="galleryViewRef"
|
||||
v-on-clickaway="onClose"
|
||||
class="bg-white dark:bg-slate-900 flex flex-col h-[inherit] w-[inherit] overflow-hidden"
|
||||
@click="onClose"
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import {
|
||||
getSortedAgentsByAvailability,
|
||||
getAgentsByUpdatedPresence,
|
||||
} from 'dashboard/helper/agentHelper.js';
|
||||
import MenuItem from './menuItem.vue';
|
||||
import MenuItemWithSubmenu from './menuItemWithSubmenu.vue';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import agentMixin from 'dashboard/mixins/agentMixin';
|
||||
import { mapGetters } from 'vuex';
|
||||
import AgentLoadingPlaceholder from './agentLoadingPlaceholder.vue';
|
||||
export default {
|
||||
components: {
|
||||
@@ -11,7 +14,6 @@ export default {
|
||||
MenuItemWithSubmenu,
|
||||
AgentLoadingPlaceholder,
|
||||
},
|
||||
mixins: [agentMixin],
|
||||
props: {
|
||||
chatId: {
|
||||
type: Number,
|
||||
@@ -112,13 +114,19 @@ export default {
|
||||
labels: 'labels/getLabels',
|
||||
teams: 'teams/getTeams',
|
||||
assignableAgentsUiFlags: 'inboxAssignableAgents/getUIFlags',
|
||||
currentUser: 'getCurrentUser',
|
||||
currentAccountId: 'getCurrentAccountId',
|
||||
}),
|
||||
filteredAgentOnAvailability() {
|
||||
const agents = this.$store.getters[
|
||||
'inboxAssignableAgents/getAssignableAgents'
|
||||
](this.inboxId);
|
||||
const agentsByUpdatedPresence = this.getAgentsByUpdatedPresence(agents);
|
||||
const filteredAgents = this.sortedAgentsByAvailability(
|
||||
const agentsByUpdatedPresence = getAgentsByUpdatedPresence(
|
||||
agents,
|
||||
this.currentUser,
|
||||
this.currentAccountId
|
||||
);
|
||||
const filteredAgents = getSortedAgentsByAvailability(
|
||||
agentsByUpdatedPresence
|
||||
);
|
||||
return filteredAgents;
|
||||
|
||||
+13
-3
@@ -2,7 +2,9 @@
|
||||
// components
|
||||
import WootButton from '../../../ui/WootButton.vue';
|
||||
import Avatar from '../../Avatar.vue';
|
||||
import aiMixin from 'dashboard/mixins/aiMixin';
|
||||
|
||||
// composables
|
||||
import { useAI } from 'dashboard/composables/useAI';
|
||||
|
||||
// store & api
|
||||
import { mapGetters } from 'vuex';
|
||||
@@ -18,7 +20,6 @@ export default {
|
||||
Avatar,
|
||||
WootButton,
|
||||
},
|
||||
mixins: [aiMixin],
|
||||
props: {
|
||||
suggestedLabels: {
|
||||
type: Array,
|
||||
@@ -30,6 +31,11 @@ export default {
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { isAIIntegrationEnabled } = useAI();
|
||||
|
||||
return { isAIIntegrationEnabled };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isDismissed: false,
|
||||
@@ -41,7 +47,11 @@ export default {
|
||||
...mapGetters({
|
||||
allLabels: 'labels/getLabels',
|
||||
currentAccountId: 'getCurrentAccountId',
|
||||
currentChat: 'getSelectedChat',
|
||||
}),
|
||||
conversationId() {
|
||||
return this.currentChat?.id;
|
||||
},
|
||||
labelTooltip() {
|
||||
if (this.preparedLabels.length > 1) {
|
||||
return this.$t('LABEL_MGMT.SUGGESTIONS.TOOLTIP.MULTIPLE_SUGGESTION');
|
||||
@@ -162,7 +172,7 @@ export default {
|
||||
delay: { show: 600, hide: 0 },
|
||||
hideOnClick: true,
|
||||
}"
|
||||
class="label-suggestion--option"
|
||||
class="label-suggestion--option !px-0"
|
||||
@click="pushOrAddLabel(label.title)"
|
||||
>
|
||||
<woot-label
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import {
|
||||
CMD_BULK_ACTION_SNOOZE_CONVERSATION,
|
||||
CMD_BULK_ACTION_REOPEN_CONVERSATION,
|
||||
CMD_BULK_ACTION_RESOLVE_CONVERSATION,
|
||||
} from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
|
||||
} from 'dashboard/helper/commandbar/events';
|
||||
|
||||
import AgentSelector from './AgentSelector.vue';
|
||||
import UpdateActions from './UpdateActions.vue';
|
||||
|
||||
@@ -48,11 +48,7 @@ const loadLinkedIssue = async () => {
|
||||
const issues = response.data;
|
||||
linkedIssue.value = issues && issues.length ? issues[0] : null;
|
||||
} catch (error) {
|
||||
const errorMessage = parseLinearAPIErrorResponse(
|
||||
error,
|
||||
t('INTEGRATION_SETTINGS.LINEAR.LOADING_ERROR')
|
||||
);
|
||||
useAlert(errorMessage);
|
||||
// We don't want to show an error message here, as it's not critical. When someone clicks on the Linear icon, we can inform them that the integration is disabled.
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ const onSelect = () => {
|
||||
};
|
||||
|
||||
useKeyboardNavigableList({
|
||||
elementRef: mentionsListContainerRef,
|
||||
items: computed(() => props.items),
|
||||
onSelect,
|
||||
adjustScroll,
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
export const mockAssignableAgents = [
|
||||
{
|
||||
id: 1,
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
auto_offline: true,
|
||||
confirmed: true,
|
||||
email: 'john@doe.com',
|
||||
available_name: 'John Doe',
|
||||
name: 'John Doe',
|
||||
role: 'administrator',
|
||||
thumbnail: '',
|
||||
},
|
||||
];
|
||||
|
||||
export const mockCurrentChat = {
|
||||
meta: {
|
||||
sender: {
|
||||
additional_attributes: {},
|
||||
availability_status: 'offline',
|
||||
email: null,
|
||||
id: 212,
|
||||
name: 'Chatwoot',
|
||||
phone_number: null,
|
||||
identifier: null,
|
||||
thumbnail: '',
|
||||
custom_attributes: {},
|
||||
last_activity_at: 1723553344,
|
||||
created_at: 1722588710,
|
||||
},
|
||||
channel: 'Channel::WebWidget',
|
||||
assignee: {
|
||||
id: 1,
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
auto_offline: true,
|
||||
confirmed: true,
|
||||
email: 'john@doe.com',
|
||||
available_name: 'John Doe',
|
||||
name: 'John Doe',
|
||||
role: 'administrator',
|
||||
thumbnail: '',
|
||||
},
|
||||
hmac_verified: false,
|
||||
},
|
||||
id: 138,
|
||||
messages: [
|
||||
{
|
||||
id: 3348,
|
||||
content: 'Hello, how can I assist you today?',
|
||||
account_id: 1,
|
||||
inbox_id: 1,
|
||||
conversation_id: 138,
|
||||
message_type: 1,
|
||||
created_at: 1724398739,
|
||||
updated_at: '2024-08-23T07:38:59.763Z',
|
||||
private: false,
|
||||
status: 'sent',
|
||||
source_id: null,
|
||||
content_type: 'text',
|
||||
content_attributes: {},
|
||||
sender_type: 'User',
|
||||
sender_id: 1,
|
||||
external_source_ids: {},
|
||||
additional_attributes: {},
|
||||
processed_message_content: 'Hello, how can I assist you today?',
|
||||
sentiment: {},
|
||||
conversation: {
|
||||
assignee_id: 1,
|
||||
unread_count: 0,
|
||||
last_activity_at: 1724398739,
|
||||
contact_inbox: {
|
||||
source_id: '5e57317d-053b-4a72-8292-a25b9f29c401',
|
||||
},
|
||||
},
|
||||
sender: {
|
||||
id: 1,
|
||||
name: 'John Doe',
|
||||
available_name: 'John Doe',
|
||||
avatar_url: '',
|
||||
type: 'user',
|
||||
availability_status: 'online',
|
||||
thumbnail: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
account_id: 1,
|
||||
uuid: '69dd6922-2f0c-4317-8796-bbeb3679cead',
|
||||
additional_attributes: {
|
||||
browser: {
|
||||
device_name: 'Unknown',
|
||||
browser_name: 'Chrome',
|
||||
platform_name: 'macOS',
|
||||
browser_version: '127.0.0.0',
|
||||
platform_version: '10.15.7',
|
||||
},
|
||||
referer: 'http://chatwoot.com/widget_tests?dark_mode=auto',
|
||||
initiated_at: {
|
||||
timestamp: 'Fri Aug 02 2024 15:21:18 GMT+0530 (India Standard Time)',
|
||||
},
|
||||
browser_language: 'en',
|
||||
},
|
||||
agent_last_seen_at: 1724400730,
|
||||
assignee_last_seen_at: 1724400686,
|
||||
can_reply: true,
|
||||
contact_last_seen_at: 1723553351,
|
||||
custom_attributes: {},
|
||||
inbox_id: 1,
|
||||
labels: ['billing'],
|
||||
muted: false,
|
||||
snoozed_until: null,
|
||||
status: 'open',
|
||||
created_at: 1722592278,
|
||||
timestamp: 1724398739,
|
||||
first_reply_created_at: 1722592316,
|
||||
unread_count: 0,
|
||||
last_non_activity_message: {},
|
||||
last_activity_at: 1724398739,
|
||||
priority: null,
|
||||
waiting_since: 0,
|
||||
sla_policy_id: 10,
|
||||
applied_sla: {
|
||||
id: 143,
|
||||
sla_id: 10,
|
||||
sla_status: 'missed',
|
||||
created_at: 1722592279,
|
||||
updated_at: 1722874214,
|
||||
sla_description: '',
|
||||
sla_name: 'Hacker SLA',
|
||||
sla_first_response_time_threshold: 600,
|
||||
sla_next_response_time_threshold: 240,
|
||||
sla_only_during_business_hours: false,
|
||||
sla_resolution_time_threshold: 259200,
|
||||
},
|
||||
sla_events: [
|
||||
{
|
||||
id: 270,
|
||||
event_type: 'nrt',
|
||||
meta: {
|
||||
message_id: 2743,
|
||||
},
|
||||
updated_at: 1722592819,
|
||||
created_at: 1722592819,
|
||||
},
|
||||
{
|
||||
id: 275,
|
||||
event_type: 'rt',
|
||||
meta: {},
|
||||
updated_at: 1722852322,
|
||||
created_at: 1722852322,
|
||||
},
|
||||
],
|
||||
allMessagesLoaded: false,
|
||||
dataFetched: true,
|
||||
};
|
||||
|
||||
export const mockTeamsList = [
|
||||
{
|
||||
id: 5,
|
||||
name: 'design',
|
||||
description: 'design team',
|
||||
allow_auto_assign: true,
|
||||
account_id: 1,
|
||||
is_member: false,
|
||||
},
|
||||
];
|
||||
|
||||
export const mockActiveLabels = [
|
||||
{
|
||||
id: 16,
|
||||
title: 'billing',
|
||||
description: '',
|
||||
color: '#D8EA19',
|
||||
show_on_sidebar: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const mockInactiveLabels = [
|
||||
{
|
||||
id: 2,
|
||||
title: 'Feature Request',
|
||||
description: '',
|
||||
color: '#D8EA19',
|
||||
show_on_sidebar: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const MOCK_FEATURE_FLAGS = {
|
||||
CRM: 'crm',
|
||||
AGENT_MANAGEMENT: 'agent_management',
|
||||
TEAM_MANAGEMENT: 'team_management',
|
||||
INBOX_MANAGEMENT: 'inbox_management',
|
||||
REPORTS: 'reports',
|
||||
LABELS: 'labels',
|
||||
CANNED_RESPONSES: 'canned_responses',
|
||||
INTEGRATIONS: 'integrations',
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
import { useAppearanceHotKeys } from '../useAppearanceHotKeys';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||
import { setColorTheme } from 'dashboard/helper/themeHelper.js';
|
||||
|
||||
vi.mock('dashboard/composables/useI18n');
|
||||
vi.mock('shared/helpers/localStorage');
|
||||
vi.mock('dashboard/helper/themeHelper.js');
|
||||
|
||||
describe('useAppearanceHotKeys', () => {
|
||||
beforeEach(() => {
|
||||
useI18n.mockReturnValue({
|
||||
t: vi.fn(key => key),
|
||||
});
|
||||
|
||||
window.matchMedia = vi.fn().mockReturnValue({ matches: false });
|
||||
});
|
||||
|
||||
it('should return goToAppearanceHotKeys computed property', () => {
|
||||
const { goToAppearanceHotKeys } = useAppearanceHotKeys();
|
||||
expect(goToAppearanceHotKeys.value).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have the correct number of appearance options', () => {
|
||||
const { goToAppearanceHotKeys } = useAppearanceHotKeys();
|
||||
expect(goToAppearanceHotKeys.value.length).toBe(4); // 1 parent + 3 theme options
|
||||
});
|
||||
|
||||
it('should have the correct parent option', () => {
|
||||
const { goToAppearanceHotKeys } = useAppearanceHotKeys();
|
||||
const parentOption = goToAppearanceHotKeys.value.find(
|
||||
option => option.id === 'appearance_settings'
|
||||
);
|
||||
expect(parentOption).toBeDefined();
|
||||
expect(parentOption.children.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should have the correct theme options', () => {
|
||||
const { goToAppearanceHotKeys } = useAppearanceHotKeys();
|
||||
const themeOptions = goToAppearanceHotKeys.value.filter(
|
||||
option => option.parent === 'appearance_settings'
|
||||
);
|
||||
expect(themeOptions.length).toBe(3);
|
||||
expect(themeOptions.map(option => option.id)).toEqual([
|
||||
'light',
|
||||
'dark',
|
||||
'auto',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should call setAppearance when a theme option is selected', () => {
|
||||
const { goToAppearanceHotKeys } = useAppearanceHotKeys();
|
||||
const lightThemeOption = goToAppearanceHotKeys.value.find(
|
||||
option => option.id === 'light'
|
||||
);
|
||||
|
||||
lightThemeOption.handler();
|
||||
|
||||
expect(LocalStorage.set).toHaveBeenCalledWith(
|
||||
LOCAL_STORAGE_KEYS.COLOR_SCHEME,
|
||||
'light'
|
||||
);
|
||||
expect(setColorTheme).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('should handle system dark mode preference', () => {
|
||||
window.matchMedia = vi.fn().mockReturnValue({ matches: true });
|
||||
|
||||
const { goToAppearanceHotKeys } = useAppearanceHotKeys();
|
||||
const autoThemeOption = goToAppearanceHotKeys.value.find(
|
||||
option => option.id === 'auto'
|
||||
);
|
||||
|
||||
autoThemeOption.handler();
|
||||
|
||||
expect(LocalStorage.set).toHaveBeenCalledWith(
|
||||
LOCAL_STORAGE_KEYS.COLOR_SCHEME,
|
||||
'auto'
|
||||
);
|
||||
expect(setColorTheme).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
import { useBulkActionsHotKeys } from '../useBulkActionsHotKeys';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
vi.mock('dashboard/composables/store');
|
||||
vi.mock('dashboard/composables/useI18n');
|
||||
vi.mock('shared/helpers/mitt');
|
||||
|
||||
describe('useBulkActionsHotKeys', () => {
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
store = {
|
||||
getters: {
|
||||
'bulkActions/getSelectedConversationIds': [],
|
||||
},
|
||||
};
|
||||
|
||||
useStore.mockReturnValue(store);
|
||||
useMapGetter.mockImplementation(key => ({
|
||||
value: store.getters[key],
|
||||
}));
|
||||
|
||||
useI18n.mockReturnValue({ t: vi.fn(key => key) });
|
||||
emitter.emit = vi.fn();
|
||||
});
|
||||
|
||||
it('should return bulk actions when conversations are selected', () => {
|
||||
store.getters['bulkActions/getSelectedConversationIds'] = [1, 2, 3];
|
||||
const { bulkActionsHotKeys } = useBulkActionsHotKeys();
|
||||
|
||||
expect(bulkActionsHotKeys.value.length).toBeGreaterThan(0);
|
||||
expect(bulkActionsHotKeys.value).toContainEqual(
|
||||
expect.objectContaining({
|
||||
id: 'bulk_action_snooze_conversation',
|
||||
title: 'COMMAND_BAR.COMMANDS.SNOOZE_CONVERSATION',
|
||||
section: 'COMMAND_BAR.SECTIONS.BULK_ACTIONS',
|
||||
})
|
||||
);
|
||||
expect(bulkActionsHotKeys.value).toContainEqual(
|
||||
expect.objectContaining({
|
||||
id: 'bulk_action_reopen_conversation',
|
||||
title: 'COMMAND_BAR.COMMANDS.REOPEN_CONVERSATION',
|
||||
section: 'COMMAND_BAR.SECTIONS.BULK_ACTIONS',
|
||||
})
|
||||
);
|
||||
expect(bulkActionsHotKeys.value).toContainEqual(
|
||||
expect.objectContaining({
|
||||
id: 'bulk_action_resolve_conversation',
|
||||
title: 'COMMAND_BAR.COMMANDS.RESOLVE_CONVERSATION',
|
||||
section: 'COMMAND_BAR.SECTIONS.BULK_ACTIONS',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should include snooze options in bulk actions', () => {
|
||||
store.getters['bulkActions/getSelectedConversationIds'] = [1, 2, 3];
|
||||
const { bulkActionsHotKeys } = useBulkActionsHotKeys();
|
||||
|
||||
const snoozeAction = bulkActionsHotKeys.value.find(
|
||||
action => action.id === 'bulk_action_snooze_conversation'
|
||||
);
|
||||
expect(snoozeAction).toBeDefined();
|
||||
expect(snoozeAction.children).toEqual(
|
||||
Object.values(wootConstants.SNOOZE_OPTIONS)
|
||||
);
|
||||
});
|
||||
|
||||
it('should create handlers for reopen and resolve actions', () => {
|
||||
store.getters['bulkActions/getSelectedConversationIds'] = [1, 2, 3];
|
||||
const { bulkActionsHotKeys } = useBulkActionsHotKeys();
|
||||
|
||||
const reopenAction = bulkActionsHotKeys.value.find(
|
||||
action => action.id === 'bulk_action_reopen_conversation'
|
||||
);
|
||||
const resolveAction = bulkActionsHotKeys.value.find(
|
||||
action => action.id === 'bulk_action_resolve_conversation'
|
||||
);
|
||||
|
||||
expect(reopenAction.handler).toBeDefined();
|
||||
expect(resolveAction.handler).toBeDefined();
|
||||
|
||||
reopenAction.handler();
|
||||
expect(emitter.emit).toHaveBeenCalledWith(
|
||||
'CMD_BULK_ACTION_REOPEN_CONVERSATION'
|
||||
);
|
||||
|
||||
resolveAction.handler();
|
||||
expect(emitter.emit).toHaveBeenCalledWith(
|
||||
'CMD_BULK_ACTION_RESOLVE_CONVERSATION'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return an empty array when no conversations are selected', () => {
|
||||
store.getters['bulkActions/getSelectedConversationIds'] = [];
|
||||
const { bulkActionsHotKeys } = useBulkActionsHotKeys();
|
||||
|
||||
expect(bulkActionsHotKeys.value).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,204 @@
|
||||
import { useConversationHotKeys } from '../useConversationHotKeys';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { useRoute } from 'dashboard/composables/route';
|
||||
import { useConversationLabels } from 'dashboard/composables/useConversationLabels';
|
||||
import { useAI } from 'dashboard/composables/useAI';
|
||||
import { useAgentsList } from 'dashboard/composables/useAgentsList';
|
||||
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
|
||||
import {
|
||||
mockAssignableAgents,
|
||||
mockCurrentChat,
|
||||
mockTeamsList,
|
||||
mockActiveLabels,
|
||||
mockInactiveLabels,
|
||||
} from './fixtures';
|
||||
|
||||
vi.mock('dashboard/composables/store');
|
||||
vi.mock('dashboard/composables/useI18n');
|
||||
vi.mock('dashboard/composables/route');
|
||||
vi.mock('dashboard/composables/useConversationLabels');
|
||||
vi.mock('dashboard/composables/useAI');
|
||||
vi.mock('dashboard/composables/useAgentsList');
|
||||
|
||||
describe('useConversationHotKeys', () => {
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
store = {
|
||||
dispatch: vi.fn(),
|
||||
getters: {
|
||||
getSelectedChat: mockCurrentChat,
|
||||
'draftMessages/getReplyEditorMode': REPLY_EDITOR_MODES.REPLY,
|
||||
getContextMenuChatId: null,
|
||||
'teams/getTeams': mockTeamsList,
|
||||
'draftMessages/get': vi.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
useStore.mockReturnValue(store);
|
||||
useMapGetter.mockImplementation(key => ({
|
||||
value: store.getters[key],
|
||||
}));
|
||||
|
||||
useI18n.mockReturnValue({ t: vi.fn(key => key) });
|
||||
useRoute.mockReturnValue({ name: 'inbox_conversation' });
|
||||
useConversationLabels.mockReturnValue({
|
||||
activeLabels: { value: mockActiveLabels },
|
||||
inactiveLabels: { value: mockInactiveLabels },
|
||||
addLabelToConversation: vi.fn(),
|
||||
removeLabelFromConversation: vi.fn(),
|
||||
});
|
||||
useAI.mockReturnValue({ isAIIntegrationEnabled: { value: true } });
|
||||
useAgentsList.mockReturnValue({
|
||||
agentsList: { value: [] },
|
||||
assignableAgents: { value: mockAssignableAgents },
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the correct computed properties', () => {
|
||||
const { conversationHotKeys } = useConversationHotKeys();
|
||||
|
||||
expect(conversationHotKeys.value).toBeDefined();
|
||||
});
|
||||
|
||||
it('should generate conversation hot keys', () => {
|
||||
const { conversationHotKeys } = useConversationHotKeys();
|
||||
expect(conversationHotKeys.value.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should include AI assist actions when AI integration is enabled', () => {
|
||||
const { conversationHotKeys } = useConversationHotKeys();
|
||||
const aiAssistAction = conversationHotKeys.value.find(
|
||||
action => action.id === 'ai_assist'
|
||||
);
|
||||
expect(aiAssistAction).toBeDefined();
|
||||
});
|
||||
|
||||
it('should not include AI assist actions when AI integration is disabled', () => {
|
||||
useAI.mockReturnValue({ isAIIntegrationEnabled: { value: false } });
|
||||
const { conversationHotKeys } = useConversationHotKeys();
|
||||
const aiAssistAction = conversationHotKeys.value.find(
|
||||
action => action.id === 'ai_assist'
|
||||
);
|
||||
expect(aiAssistAction).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should dispatch actions when handlers are called', () => {
|
||||
const { conversationHotKeys } = useConversationHotKeys();
|
||||
const assignAgentAction = conversationHotKeys.value.find(
|
||||
action => action.id === 'assign_an_agent'
|
||||
);
|
||||
expect(assignAgentAction).toBeDefined();
|
||||
|
||||
if (assignAgentAction && assignAgentAction.children) {
|
||||
const childAction = conversationHotKeys.value.find(
|
||||
action => action.id === assignAgentAction.children[0]
|
||||
);
|
||||
if (childAction && childAction.handler) {
|
||||
childAction.handler({ agentInfo: { id: 2 } });
|
||||
expect(store.dispatch).toHaveBeenCalledWith('assignAgent', {
|
||||
conversationId: 1,
|
||||
agentId: 2,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should return snooze actions when in snooze context', () => {
|
||||
store.getters.getContextMenuChatId = 1;
|
||||
useMapGetter.mockImplementation(key => ({
|
||||
value: store.getters[key],
|
||||
}));
|
||||
useRoute.mockReturnValue({ name: 'inbox_conversation' });
|
||||
|
||||
const { conversationHotKeys } = useConversationHotKeys();
|
||||
const snoozeAction = conversationHotKeys.value.find(action =>
|
||||
action.id.includes('snooze_conversation')
|
||||
);
|
||||
expect(snoozeAction).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return the correct label actions when there are active labels', () => {
|
||||
const { conversationHotKeys } = useConversationHotKeys();
|
||||
const addLabelAction = conversationHotKeys.value.find(
|
||||
action => action.id === 'add_a_label_to_the_conversation'
|
||||
);
|
||||
const removeLabelAction = conversationHotKeys.value.find(
|
||||
action => action.id === 'remove_a_label_to_the_conversation'
|
||||
);
|
||||
|
||||
expect(addLabelAction).toBeDefined();
|
||||
expect(removeLabelAction).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return only add label actions when there are no active labels', () => {
|
||||
useConversationLabels.mockReturnValue({
|
||||
activeLabels: { value: [] },
|
||||
inactiveLabels: { value: [{ title: 'inactive_label' }] },
|
||||
addLabelToConversation: vi.fn(),
|
||||
removeLabelFromConversation: vi.fn(),
|
||||
});
|
||||
|
||||
const { conversationHotKeys } = useConversationHotKeys();
|
||||
const addLabelAction = conversationHotKeys.value.find(
|
||||
action => action.id === 'add_a_label_to_the_conversation'
|
||||
);
|
||||
const removeLabelAction = conversationHotKeys.value.find(
|
||||
action => action.id === 'remove_a_label_to_the_conversation'
|
||||
);
|
||||
|
||||
expect(addLabelAction).toBeDefined();
|
||||
expect(removeLabelAction).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return the correct team assignment actions', () => {
|
||||
const { conversationHotKeys } = useConversationHotKeys();
|
||||
const assignTeamAction = conversationHotKeys.value.find(
|
||||
action => action.id === 'assign_a_team'
|
||||
);
|
||||
|
||||
expect(assignTeamAction).toBeDefined();
|
||||
expect(assignTeamAction.children.length).toBe(mockTeamsList.length);
|
||||
});
|
||||
|
||||
it('should return the correct priority assignment actions', () => {
|
||||
const { conversationHotKeys } = useConversationHotKeys();
|
||||
const assignPriorityAction = conversationHotKeys.value.find(
|
||||
action => action.id === 'assign_priority'
|
||||
);
|
||||
|
||||
expect(assignPriorityAction).toBeDefined();
|
||||
expect(assignPriorityAction.children.length).toBe(4);
|
||||
});
|
||||
|
||||
it('should return the correct conversation additional actions', () => {
|
||||
const { conversationHotKeys } = useConversationHotKeys();
|
||||
const muteAction = conversationHotKeys.value.find(
|
||||
action => action.id === 'mute_conversation'
|
||||
);
|
||||
const sendTranscriptAction = conversationHotKeys.value.find(
|
||||
action => action.id === 'send_transcript'
|
||||
);
|
||||
|
||||
expect(muteAction).toBeDefined();
|
||||
expect(sendTranscriptAction).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return unmute action when conversation is muted', () => {
|
||||
store.getters.getSelectedChat = { ...mockCurrentChat, muted: true };
|
||||
const { conversationHotKeys } = useConversationHotKeys();
|
||||
const unmuteAction = conversationHotKeys.value.find(
|
||||
action => action.id === 'unmute_conversation'
|
||||
);
|
||||
|
||||
expect(unmuteAction).toBeDefined();
|
||||
});
|
||||
|
||||
it('should not return conversation hot keys when not in conversation or inbox route', () => {
|
||||
useRoute.mockReturnValue({ name: 'some_other_route' });
|
||||
const { conversationHotKeys } = useConversationHotKeys();
|
||||
|
||||
expect(conversationHotKeys.value.length).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,188 @@
|
||||
import { useGoToCommandHotKeys } from '../useGoToCommandHotKeys';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { useRouter } from 'dashboard/composables/route';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||
import { MOCK_FEATURE_FLAGS } from './fixtures';
|
||||
|
||||
vi.mock('dashboard/composables/store');
|
||||
vi.mock('dashboard/composables/useI18n');
|
||||
vi.mock('dashboard/composables/route');
|
||||
vi.mock('dashboard/composables/useAdmin');
|
||||
vi.mock('dashboard/helper/URLHelper');
|
||||
|
||||
const mockRoutes = [
|
||||
{ path: 'accounts/:accountId/dashboard', name: 'dashboard' },
|
||||
{
|
||||
path: 'accounts/:accountId/contacts',
|
||||
name: 'contacts',
|
||||
featureFlag: MOCK_FEATURE_FLAGS.CRM,
|
||||
},
|
||||
{
|
||||
path: 'accounts/:accountId/settings/agents/list',
|
||||
name: 'agent_settings',
|
||||
featureFlag: MOCK_FEATURE_FLAGS.AGENT_MANAGEMENT,
|
||||
},
|
||||
{
|
||||
path: 'accounts/:accountId/settings/teams/list',
|
||||
name: 'team_settings',
|
||||
featureFlag: MOCK_FEATURE_FLAGS.TEAM_MANAGEMENT,
|
||||
},
|
||||
{
|
||||
path: 'accounts/:accountId/settings/inboxes/list',
|
||||
name: 'inbox_settings',
|
||||
featureFlag: MOCK_FEATURE_FLAGS.INBOX_MANAGEMENT,
|
||||
},
|
||||
{ path: 'accounts/:accountId/profile/settings', name: 'profile_settings' },
|
||||
{ path: 'accounts/:accountId/notifications', name: 'notifications' },
|
||||
{
|
||||
path: 'accounts/:accountId/reports/overview',
|
||||
name: 'reports_overview',
|
||||
featureFlag: MOCK_FEATURE_FLAGS.REPORTS,
|
||||
},
|
||||
{
|
||||
path: 'accounts/:accountId/settings/labels/list',
|
||||
name: 'label_settings',
|
||||
featureFlag: MOCK_FEATURE_FLAGS.LABELS,
|
||||
},
|
||||
{
|
||||
path: 'accounts/:accountId/settings/canned-response/list',
|
||||
name: 'canned_responses',
|
||||
featureFlag: MOCK_FEATURE_FLAGS.CANNED_RESPONSES,
|
||||
},
|
||||
{
|
||||
path: 'accounts/:accountId/settings/applications',
|
||||
name: 'applications',
|
||||
featureFlag: MOCK_FEATURE_FLAGS.INTEGRATIONS,
|
||||
},
|
||||
];
|
||||
|
||||
describe('useGoToCommandHotKeys', () => {
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
store = {
|
||||
getters: {
|
||||
getCurrentAccountId: 1,
|
||||
'accounts/isFeatureEnabledonAccount': vi.fn().mockReturnValue(true),
|
||||
},
|
||||
};
|
||||
|
||||
useStore.mockReturnValue(store);
|
||||
useMapGetter.mockImplementation(key => ({
|
||||
value: store.getters[key],
|
||||
}));
|
||||
|
||||
useI18n.mockReturnValue({ t: vi.fn(key => key) });
|
||||
useRouter.mockReturnValue({ push: vi.fn() });
|
||||
useAdmin.mockReturnValue({ isAdmin: { value: true } });
|
||||
frontendURL.mockImplementation(url => url);
|
||||
});
|
||||
|
||||
it('should return goToCommandHotKeys computed property', () => {
|
||||
const { goToCommandHotKeys } = useGoToCommandHotKeys();
|
||||
expect(goToCommandHotKeys.value).toBeDefined();
|
||||
expect(goToCommandHotKeys.value.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should filter commands based on feature flags', () => {
|
||||
store.getters['accounts/isFeatureEnabledonAccount'] = vi.fn(
|
||||
(accountId, flag) => flag !== MOCK_FEATURE_FLAGS.CRM
|
||||
);
|
||||
const { goToCommandHotKeys } = useGoToCommandHotKeys();
|
||||
|
||||
mockRoutes.forEach(route => {
|
||||
const command = goToCommandHotKeys.value.find(cmd =>
|
||||
cmd.id.includes(route.name)
|
||||
);
|
||||
if (route.featureFlag === MOCK_FEATURE_FLAGS.CRM) {
|
||||
expect(command).toBeUndefined();
|
||||
} else if (!route.featureFlag) {
|
||||
expect(command).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter commands for non-admin users', () => {
|
||||
useAdmin.mockReturnValue({ isAdmin: { value: false } });
|
||||
const { goToCommandHotKeys } = useGoToCommandHotKeys();
|
||||
|
||||
const adminOnlyCommands = goToCommandHotKeys.value.filter(
|
||||
cmd =>
|
||||
cmd.id.includes('agent_settings') ||
|
||||
cmd.id.includes('team_settings') ||
|
||||
cmd.id.includes('inbox_settings')
|
||||
);
|
||||
expect(adminOnlyCommands.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should include commands for both admin and agent roles when user is admin', () => {
|
||||
const { goToCommandHotKeys } = useGoToCommandHotKeys();
|
||||
const adminCommand = goToCommandHotKeys.value.find(cmd =>
|
||||
cmd.id.includes('agent_settings')
|
||||
);
|
||||
const agentCommand = goToCommandHotKeys.value.find(cmd =>
|
||||
cmd.id.includes('profile_settings')
|
||||
);
|
||||
expect(adminCommand).toBeDefined();
|
||||
expect(agentCommand).toBeDefined();
|
||||
});
|
||||
|
||||
it('should translate section and title for each command', () => {
|
||||
const { goToCommandHotKeys } = useGoToCommandHotKeys();
|
||||
goToCommandHotKeys.value.forEach(command => {
|
||||
expect(useI18n().t).toHaveBeenCalledWith(
|
||||
expect.stringContaining('COMMAND_BAR.SECTIONS.')
|
||||
);
|
||||
expect(useI18n().t).toHaveBeenCalledWith(
|
||||
expect.stringContaining('COMMAND_BAR.COMMANDS.')
|
||||
);
|
||||
expect(command.section).toBeDefined();
|
||||
expect(command.title).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should call router.push with correct URL when handler is called', () => {
|
||||
const { goToCommandHotKeys } = useGoToCommandHotKeys();
|
||||
goToCommandHotKeys.value.forEach(command => {
|
||||
command.handler();
|
||||
expect(useRouter().push).toHaveBeenCalledWith(expect.any(String));
|
||||
});
|
||||
});
|
||||
|
||||
it('should use current account ID in the path', () => {
|
||||
store.getters.getCurrentAccountId = 42;
|
||||
const { goToCommandHotKeys } = useGoToCommandHotKeys();
|
||||
goToCommandHotKeys.value.forEach(command => {
|
||||
command.handler();
|
||||
expect(useRouter().push).toHaveBeenCalledWith(
|
||||
expect.stringContaining('42')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should include icon for each command', () => {
|
||||
const { goToCommandHotKeys } = useGoToCommandHotKeys();
|
||||
goToCommandHotKeys.value.forEach(command => {
|
||||
expect(command.icon).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return commands for all enabled features', () => {
|
||||
const { goToCommandHotKeys } = useGoToCommandHotKeys();
|
||||
const enabledFeatureCommands = goToCommandHotKeys.value.filter(cmd =>
|
||||
mockRoutes.some(route => route.featureFlag && cmd.id.includes(route.name))
|
||||
);
|
||||
expect(enabledFeatureCommands.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should not return commands for disabled features', () => {
|
||||
store.getters['accounts/isFeatureEnabledonAccount'] = vi.fn(() => false);
|
||||
const { goToCommandHotKeys } = useGoToCommandHotKeys();
|
||||
const disabledFeatureCommands = goToCommandHotKeys.value.filter(cmd =>
|
||||
mockRoutes.some(route => route.featureFlag && cmd.id.includes(route.name))
|
||||
);
|
||||
expect(disabledFeatureCommands.length).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useInboxHotKeys } from '../useInboxHotKeys';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { useRoute } from 'dashboard/composables/route';
|
||||
import { isAInboxViewRoute } from 'dashboard/helper/routeHelpers';
|
||||
|
||||
vi.mock('dashboard/composables/useI18n');
|
||||
vi.mock('dashboard/composables/route');
|
||||
vi.mock('dashboard/helper/routeHelpers');
|
||||
vi.mock('shared/helpers/mitt');
|
||||
|
||||
describe('useInboxHotKeys', () => {
|
||||
beforeEach(() => {
|
||||
useI18n.mockReturnValue({ t: vi.fn(key => key) });
|
||||
useRoute.mockReturnValue({ name: 'inbox_dashboard' });
|
||||
isAInboxViewRoute.mockReturnValue(true);
|
||||
});
|
||||
|
||||
it('should return inbox hot keys when on an inbox view route', () => {
|
||||
const { inboxHotKeys } = useInboxHotKeys();
|
||||
expect(inboxHotKeys.value.length).toBeGreaterThan(0);
|
||||
expect(inboxHotKeys.value[0].id).toBe('snooze_notification');
|
||||
});
|
||||
|
||||
it('should return an empty array when not on an inbox view route', () => {
|
||||
isAInboxViewRoute.mockReturnValue(false);
|
||||
const { inboxHotKeys } = useInboxHotKeys();
|
||||
expect(inboxHotKeys.value).toEqual([]);
|
||||
});
|
||||
|
||||
it('should have the correct structure for snooze actions', () => {
|
||||
const { inboxHotKeys } = useInboxHotKeys();
|
||||
const snoozeNotificationAction = inboxHotKeys.value.find(
|
||||
action => action.id === 'snooze_notification'
|
||||
);
|
||||
expect(snoozeNotificationAction).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import {
|
||||
ICON_APPEARANCE,
|
||||
ICON_LIGHT_MODE,
|
||||
ICON_DARK_MODE,
|
||||
ICON_SYSTEM_MODE,
|
||||
} from 'dashboard/helper/commandbar/icons';
|
||||
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||
import { setColorTheme } from 'dashboard/helper/themeHelper.js';
|
||||
|
||||
const getThemeOptions = t => [
|
||||
{
|
||||
key: 'light',
|
||||
label: t('COMMAND_BAR.COMMANDS.LIGHT_MODE'),
|
||||
icon: ICON_LIGHT_MODE,
|
||||
},
|
||||
{
|
||||
key: 'dark',
|
||||
label: t('COMMAND_BAR.COMMANDS.DARK_MODE'),
|
||||
icon: ICON_DARK_MODE,
|
||||
},
|
||||
{
|
||||
key: 'auto',
|
||||
label: t('COMMAND_BAR.COMMANDS.SYSTEM_MODE'),
|
||||
icon: ICON_SYSTEM_MODE,
|
||||
},
|
||||
];
|
||||
|
||||
const setAppearance = theme => {
|
||||
LocalStorage.set(LOCAL_STORAGE_KEYS.COLOR_SCHEME, theme);
|
||||
const isOSOnDarkMode = window.matchMedia(
|
||||
'(prefers-color-scheme: dark)'
|
||||
).matches;
|
||||
setColorTheme(isOSOnDarkMode);
|
||||
};
|
||||
|
||||
export function useAppearanceHotKeys() {
|
||||
const { t } = useI18n();
|
||||
|
||||
const themeOptions = computed(() => getThemeOptions(t));
|
||||
|
||||
const goToAppearanceHotKeys = computed(() => {
|
||||
const options = themeOptions.value.map(theme => ({
|
||||
id: theme.key,
|
||||
title: theme.label,
|
||||
parent: 'appearance_settings',
|
||||
section: t('COMMAND_BAR.SECTIONS.APPEARANCE'),
|
||||
icon: theme.icon,
|
||||
handler: () => {
|
||||
setAppearance(theme.key);
|
||||
},
|
||||
}));
|
||||
return [
|
||||
{
|
||||
id: 'appearance_settings',
|
||||
title: t('COMMAND_BAR.COMMANDS.CHANGE_APPEARANCE'),
|
||||
section: t('COMMAND_BAR.SECTIONS.APPEARANCE'),
|
||||
icon: ICON_APPEARANCE,
|
||||
children: options.map(option => option.id),
|
||||
},
|
||||
...options,
|
||||
];
|
||||
});
|
||||
|
||||
return {
|
||||
goToAppearanceHotKeys,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
|
||||
import {
|
||||
CMD_BULK_ACTION_SNOOZE_CONVERSATION,
|
||||
CMD_BULK_ACTION_REOPEN_CONVERSATION,
|
||||
CMD_BULK_ACTION_RESOLVE_CONVERSATION,
|
||||
} from 'dashboard/helper/commandbar/events';
|
||||
import {
|
||||
ICON_SNOOZE_CONVERSATION,
|
||||
ICON_REOPEN_CONVERSATION,
|
||||
ICON_RESOLVE_CONVERSATION,
|
||||
} from 'dashboard/helper/commandbar/icons';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
import { createSnoozeHandlers } from 'dashboard/helper/commandbar/actions';
|
||||
|
||||
const SNOOZE_OPTIONS = wootConstants.SNOOZE_OPTIONS;
|
||||
|
||||
const createEmitHandler = event => () => emitter.emit(event);
|
||||
|
||||
const SNOOZE_CONVERSATION_BULK_ACTIONS = [
|
||||
{
|
||||
id: 'bulk_action_snooze_conversation',
|
||||
title: 'COMMAND_BAR.COMMANDS.SNOOZE_CONVERSATION',
|
||||
section: 'COMMAND_BAR.SECTIONS.BULK_ACTIONS',
|
||||
icon: ICON_SNOOZE_CONVERSATION,
|
||||
children: Object.values(SNOOZE_OPTIONS),
|
||||
},
|
||||
...createSnoozeHandlers(
|
||||
CMD_BULK_ACTION_SNOOZE_CONVERSATION,
|
||||
'bulk_action_snooze_conversation',
|
||||
'COMMAND_BAR.SECTIONS.BULK_ACTIONS'
|
||||
),
|
||||
];
|
||||
|
||||
const RESOLVED_CONVERSATION_BULK_ACTIONS = [
|
||||
{
|
||||
id: 'bulk_action_reopen_conversation',
|
||||
title: 'COMMAND_BAR.COMMANDS.REOPEN_CONVERSATION',
|
||||
section: 'COMMAND_BAR.SECTIONS.BULK_ACTIONS',
|
||||
icon: ICON_REOPEN_CONVERSATION,
|
||||
handler: createEmitHandler(CMD_BULK_ACTION_REOPEN_CONVERSATION),
|
||||
},
|
||||
];
|
||||
|
||||
const OPEN_CONVERSATION_BULK_ACTIONS = [
|
||||
{
|
||||
id: 'bulk_action_resolve_conversation',
|
||||
title: 'COMMAND_BAR.COMMANDS.RESOLVE_CONVERSATION',
|
||||
section: 'COMMAND_BAR.SECTIONS.BULK_ACTIONS',
|
||||
icon: ICON_RESOLVE_CONVERSATION,
|
||||
handler: createEmitHandler(CMD_BULK_ACTION_RESOLVE_CONVERSATION),
|
||||
},
|
||||
];
|
||||
|
||||
export function useBulkActionsHotKeys() {
|
||||
const { t } = useI18n();
|
||||
|
||||
const selectedConversations = useMapGetter(
|
||||
'bulkActions/getSelectedConversationIds'
|
||||
);
|
||||
|
||||
const prepareActions = actions => {
|
||||
return actions.map(action => ({
|
||||
...action,
|
||||
title: t(action.title),
|
||||
section: t(action.section),
|
||||
}));
|
||||
};
|
||||
|
||||
const bulkActionsHotKeys = computed(() => {
|
||||
let actions = [];
|
||||
if (selectedConversations.value.length > 0) {
|
||||
actions = [
|
||||
...SNOOZE_CONVERSATION_BULK_ACTIONS,
|
||||
...RESOLVED_CONVERSATION_BULK_ACTIONS,
|
||||
...OPEN_CONVERSATION_BULK_ACTIONS,
|
||||
];
|
||||
}
|
||||
return prepareActions(actions);
|
||||
});
|
||||
|
||||
return {
|
||||
bulkActionsHotKeys,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useRoute } from 'dashboard/composables/route';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
import { useConversationLabels } from 'dashboard/composables/useConversationLabels';
|
||||
import { useAI } from 'dashboard/composables/useAI';
|
||||
import { useAgentsList } from 'dashboard/composables/useAgentsList';
|
||||
import { CMD_AI_ASSIST } from 'dashboard/helper/commandbar/events';
|
||||
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
|
||||
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
|
||||
import {
|
||||
ICON_ADD_LABEL,
|
||||
ICON_ASSIGN_AGENT,
|
||||
ICON_ASSIGN_PRIORITY,
|
||||
ICON_ASSIGN_TEAM,
|
||||
ICON_REMOVE_LABEL,
|
||||
ICON_PRIORITY_URGENT,
|
||||
ICON_PRIORITY_HIGH,
|
||||
ICON_PRIORITY_LOW,
|
||||
ICON_PRIORITY_MEDIUM,
|
||||
ICON_PRIORITY_NONE,
|
||||
ICON_AI_ASSIST,
|
||||
ICON_AI_SUMMARY,
|
||||
ICON_AI_SHORTEN,
|
||||
ICON_AI_EXPAND,
|
||||
ICON_AI_GRAMMAR,
|
||||
} from 'dashboard/helper/commandbar/icons';
|
||||
|
||||
import {
|
||||
OPEN_CONVERSATION_ACTIONS,
|
||||
SNOOZE_CONVERSATION_ACTIONS,
|
||||
RESOLVED_CONVERSATION_ACTIONS,
|
||||
SEND_TRANSCRIPT_ACTION,
|
||||
UNMUTE_ACTION,
|
||||
MUTE_ACTION,
|
||||
} from 'dashboard/helper/commandbar/actions';
|
||||
import {
|
||||
isAConversationRoute,
|
||||
isAInboxViewRoute,
|
||||
} from 'dashboard/helper/routeHelpers';
|
||||
|
||||
const prepareActions = (actions, t) => {
|
||||
return actions.map(action => ({
|
||||
...action,
|
||||
title: t(action.title),
|
||||
section: t(action.section),
|
||||
}));
|
||||
};
|
||||
|
||||
const createPriorityOptions = (t, currentPriority) => {
|
||||
return [
|
||||
{
|
||||
label: t('CONVERSATION.PRIORITY.OPTIONS.NONE'),
|
||||
key: null,
|
||||
icon: ICON_PRIORITY_NONE,
|
||||
},
|
||||
{
|
||||
label: t('CONVERSATION.PRIORITY.OPTIONS.URGENT'),
|
||||
key: 'urgent',
|
||||
icon: ICON_PRIORITY_URGENT,
|
||||
},
|
||||
{
|
||||
label: t('CONVERSATION.PRIORITY.OPTIONS.HIGH'),
|
||||
key: 'high',
|
||||
icon: ICON_PRIORITY_HIGH,
|
||||
},
|
||||
{
|
||||
label: t('CONVERSATION.PRIORITY.OPTIONS.MEDIUM'),
|
||||
key: 'medium',
|
||||
icon: ICON_PRIORITY_MEDIUM,
|
||||
},
|
||||
{
|
||||
label: t('CONVERSATION.PRIORITY.OPTIONS.LOW'),
|
||||
key: 'low',
|
||||
icon: ICON_PRIORITY_LOW,
|
||||
},
|
||||
].filter(item => item.key !== currentPriority);
|
||||
};
|
||||
|
||||
const createNonDraftMessageAIAssistActions = (t, replyMode) => {
|
||||
if (replyMode === REPLY_EDITOR_MODES.REPLY) {
|
||||
return [
|
||||
{
|
||||
label: t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.REPLY_SUGGESTION'),
|
||||
key: 'reply_suggestion',
|
||||
icon: ICON_AI_ASSIST,
|
||||
},
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
label: t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.SUMMARIZE'),
|
||||
key: 'summarize',
|
||||
icon: ICON_AI_SUMMARY,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const createDraftMessageAIAssistActions = t => {
|
||||
return [
|
||||
{
|
||||
label: t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.REPHRASE'),
|
||||
key: 'rephrase',
|
||||
icon: ICON_AI_ASSIST,
|
||||
},
|
||||
{
|
||||
label: t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.FIX_SPELLING_GRAMMAR'),
|
||||
key: 'fix_spelling_grammar',
|
||||
icon: ICON_AI_GRAMMAR,
|
||||
},
|
||||
{
|
||||
label: t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.EXPAND'),
|
||||
key: 'expand',
|
||||
icon: ICON_AI_EXPAND,
|
||||
},
|
||||
{
|
||||
label: t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.SHORTEN'),
|
||||
key: 'shorten',
|
||||
icon: ICON_AI_SHORTEN,
|
||||
},
|
||||
{
|
||||
label: t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.MAKE_FRIENDLY'),
|
||||
key: 'make_friendly',
|
||||
icon: ICON_AI_ASSIST,
|
||||
},
|
||||
{
|
||||
label: t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.MAKE_FORMAL'),
|
||||
key: 'make_formal',
|
||||
icon: ICON_AI_ASSIST,
|
||||
},
|
||||
{
|
||||
label: t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.SIMPLIFY'),
|
||||
key: 'simplify',
|
||||
icon: ICON_AI_ASSIST,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export function useConversationHotKeys() {
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
const route = useRoute();
|
||||
|
||||
const {
|
||||
activeLabels,
|
||||
inactiveLabels,
|
||||
addLabelToConversation,
|
||||
removeLabelFromConversation,
|
||||
} = useConversationLabels();
|
||||
|
||||
const { isAIIntegrationEnabled } = useAI();
|
||||
const { agentsList } = useAgentsList();
|
||||
|
||||
const currentChat = useMapGetter('getSelectedChat');
|
||||
const replyMode = useMapGetter('draftMessages/getReplyEditorMode');
|
||||
const contextMenuChatId = useMapGetter('getContextMenuChatId');
|
||||
const teams = useMapGetter('teams/getTeams');
|
||||
const getDraftMessage = useMapGetter('draftMessages/get');
|
||||
|
||||
const conversationId = computed(() => currentChat.value?.id);
|
||||
const draftKey = computed(
|
||||
() => `draft-${conversationId.value}-${replyMode.value}`
|
||||
);
|
||||
|
||||
const draftMessage = computed(() => getDraftMessage.value(draftKey.value));
|
||||
|
||||
const hasAnAssignedTeam = computed(() => !!currentChat.value?.meta?.team);
|
||||
|
||||
const teamsList = computed(() => {
|
||||
if (hasAnAssignedTeam.value) {
|
||||
return [{ id: 0, name: t('TEAMS_SETTINGS.LIST.NONE') }, ...teams.value];
|
||||
}
|
||||
return teams.value;
|
||||
});
|
||||
|
||||
const onChangeAssignee = action => {
|
||||
store.dispatch('assignAgent', {
|
||||
conversationId: currentChat.value.id,
|
||||
agentId: action.agentInfo.id,
|
||||
});
|
||||
};
|
||||
|
||||
const onChangePriority = action => {
|
||||
store.dispatch('assignPriority', {
|
||||
conversationId: currentChat.value.id,
|
||||
priority: action.priority.key,
|
||||
});
|
||||
};
|
||||
|
||||
const onChangeTeam = action => {
|
||||
store.dispatch('assignTeam', {
|
||||
conversationId: currentChat.value.id,
|
||||
teamId: action.teamInfo.id,
|
||||
});
|
||||
};
|
||||
|
||||
const statusActions = computed(() => {
|
||||
const isOpen = currentChat.value?.status === wootConstants.STATUS_TYPE.OPEN;
|
||||
const isSnoozed =
|
||||
currentChat.value?.status === wootConstants.STATUS_TYPE.SNOOZED;
|
||||
const isResolved =
|
||||
currentChat.value?.status === wootConstants.STATUS_TYPE.RESOLVED;
|
||||
|
||||
let actions = [];
|
||||
if (isOpen) {
|
||||
actions = [...OPEN_CONVERSATION_ACTIONS, ...SNOOZE_CONVERSATION_ACTIONS];
|
||||
} else if (isResolved || isSnoozed) {
|
||||
actions = RESOLVED_CONVERSATION_ACTIONS;
|
||||
}
|
||||
return prepareActions(actions, t);
|
||||
});
|
||||
|
||||
const priorityOptions = computed(() =>
|
||||
createPriorityOptions(t, currentChat.value?.priority)
|
||||
);
|
||||
|
||||
const assignAgentActions = computed(() => {
|
||||
const agentOptions = agentsList.value.map(agent => ({
|
||||
id: `agent-${agent.id}`,
|
||||
title: agent.name,
|
||||
parent: 'assign_an_agent',
|
||||
section: t('COMMAND_BAR.SECTIONS.CHANGE_ASSIGNEE'),
|
||||
agentInfo: agent,
|
||||
icon: ICON_ASSIGN_AGENT,
|
||||
handler: onChangeAssignee,
|
||||
}));
|
||||
return [
|
||||
{
|
||||
id: 'assign_an_agent',
|
||||
title: t('COMMAND_BAR.COMMANDS.ASSIGN_AN_AGENT'),
|
||||
section: t('COMMAND_BAR.SECTIONS.CONVERSATION'),
|
||||
icon: ICON_ASSIGN_AGENT,
|
||||
children: agentOptions.map(option => option.id),
|
||||
},
|
||||
...agentOptions,
|
||||
];
|
||||
});
|
||||
|
||||
const assignPriorityActions = computed(() => {
|
||||
const options = priorityOptions.value.map(priority => ({
|
||||
id: `priority-${priority.key}`,
|
||||
title: priority.label,
|
||||
parent: 'assign_priority',
|
||||
section: t('COMMAND_BAR.SECTIONS.CHANGE_PRIORITY'),
|
||||
priority: priority,
|
||||
icon: priority.icon,
|
||||
handler: onChangePriority,
|
||||
}));
|
||||
return [
|
||||
{
|
||||
id: 'assign_priority',
|
||||
title: t('COMMAND_BAR.COMMANDS.ASSIGN_PRIORITY'),
|
||||
section: t('COMMAND_BAR.SECTIONS.CONVERSATION'),
|
||||
icon: ICON_ASSIGN_PRIORITY,
|
||||
children: options.map(option => option.id),
|
||||
},
|
||||
...options,
|
||||
];
|
||||
});
|
||||
|
||||
const assignTeamActions = computed(() => {
|
||||
const teamOptions = teamsList.value.map(team => ({
|
||||
id: `team-${team.id}`,
|
||||
title: team.name,
|
||||
parent: 'assign_a_team',
|
||||
section: t('COMMAND_BAR.SECTIONS.CHANGE_TEAM'),
|
||||
teamInfo: team,
|
||||
icon: ICON_ASSIGN_TEAM,
|
||||
handler: onChangeTeam,
|
||||
}));
|
||||
return [
|
||||
{
|
||||
id: 'assign_a_team',
|
||||
title: t('COMMAND_BAR.COMMANDS.ASSIGN_A_TEAM'),
|
||||
section: t('COMMAND_BAR.SECTIONS.CONVERSATION'),
|
||||
icon: ICON_ASSIGN_TEAM,
|
||||
children: teamOptions.map(option => option.id),
|
||||
},
|
||||
...teamOptions,
|
||||
];
|
||||
});
|
||||
|
||||
const addLabelActions = computed(() => {
|
||||
const availableLabels = inactiveLabels.value.map(label => ({
|
||||
id: label.title,
|
||||
title: `#${label.title}`,
|
||||
parent: 'add_a_label_to_the_conversation',
|
||||
section: t('COMMAND_BAR.SECTIONS.ADD_LABEL'),
|
||||
icon: ICON_ADD_LABEL,
|
||||
handler: action => addLabelToConversation({ title: action.id }),
|
||||
}));
|
||||
return [
|
||||
...availableLabels,
|
||||
{
|
||||
id: 'add_a_label_to_the_conversation',
|
||||
title: t('COMMAND_BAR.COMMANDS.ADD_LABELS_TO_CONVERSATION'),
|
||||
section: t('COMMAND_BAR.SECTIONS.CONVERSATION'),
|
||||
icon: ICON_ADD_LABEL,
|
||||
children: inactiveLabels.value.map(label => label.title),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const removeLabelActions = computed(() => {
|
||||
const activeLabelsComputed = activeLabels.value.map(label => ({
|
||||
id: label.title,
|
||||
title: `#${label.title}`,
|
||||
parent: 'remove_a_label_to_the_conversation',
|
||||
section: t('COMMAND_BAR.SECTIONS.REMOVE_LABEL'),
|
||||
icon: ICON_REMOVE_LABEL,
|
||||
handler: action => removeLabelFromConversation(action.id),
|
||||
}));
|
||||
return [
|
||||
...activeLabelsComputed,
|
||||
{
|
||||
id: 'remove_a_label_to_the_conversation',
|
||||
title: t('COMMAND_BAR.COMMANDS.REMOVE_LABEL_FROM_CONVERSATION'),
|
||||
section: t('COMMAND_BAR.SECTIONS.CONVERSATION'),
|
||||
icon: ICON_REMOVE_LABEL,
|
||||
children: activeLabels.value.map(label => label.title),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const labelActions = computed(() => {
|
||||
if (activeLabels.value.length) {
|
||||
return [...addLabelActions.value, ...removeLabelActions.value];
|
||||
}
|
||||
return addLabelActions.value;
|
||||
});
|
||||
|
||||
const conversationAdditionalActions = computed(() => {
|
||||
return prepareActions(
|
||||
[
|
||||
currentChat.value.muted ? UNMUTE_ACTION : MUTE_ACTION,
|
||||
SEND_TRANSCRIPT_ACTION,
|
||||
],
|
||||
t
|
||||
);
|
||||
});
|
||||
|
||||
const AIAssistActions = computed(() => {
|
||||
const aiOptions = draftMessage.value
|
||||
? createDraftMessageAIAssistActions(t)
|
||||
: createNonDraftMessageAIAssistActions(t, replyMode.value);
|
||||
const options = aiOptions.map(item => ({
|
||||
id: `ai-assist-${item.key}`,
|
||||
title: item.label,
|
||||
parent: 'ai_assist',
|
||||
section: t('COMMAND_BAR.SECTIONS.AI_ASSIST'),
|
||||
priority: item,
|
||||
icon: item.icon,
|
||||
handler: () => emitter.emit(CMD_AI_ASSIST, item.key),
|
||||
}));
|
||||
return [
|
||||
{
|
||||
id: 'ai_assist',
|
||||
title: t('COMMAND_BAR.COMMANDS.AI_ASSIST'),
|
||||
section: t('COMMAND_BAR.SECTIONS.AI_ASSIST'),
|
||||
icon: ICON_AI_ASSIST,
|
||||
children: options.map(option => option.id),
|
||||
},
|
||||
...options,
|
||||
];
|
||||
});
|
||||
|
||||
const isConversationOrInboxRoute = computed(() => {
|
||||
return isAConversationRoute(route.name) || isAInboxViewRoute(route.name);
|
||||
});
|
||||
|
||||
const shouldShowSnoozeOption = computed(() => {
|
||||
return (
|
||||
isAConversationRoute(route.name, true, false) && contextMenuChatId.value
|
||||
);
|
||||
});
|
||||
|
||||
const getDefaultConversationHotKeys = computed(() => {
|
||||
const defaultConversationHotKeys = [
|
||||
...statusActions.value,
|
||||
...conversationAdditionalActions.value,
|
||||
...assignAgentActions.value,
|
||||
...assignTeamActions.value,
|
||||
...labelActions.value,
|
||||
...assignPriorityActions.value,
|
||||
];
|
||||
if (isAIIntegrationEnabled.value) {
|
||||
return [...defaultConversationHotKeys, ...AIAssistActions.value];
|
||||
}
|
||||
return defaultConversationHotKeys;
|
||||
});
|
||||
|
||||
const conversationHotKeys = computed(() => {
|
||||
if (shouldShowSnoozeOption.value) {
|
||||
return prepareActions(SNOOZE_CONVERSATION_ACTIONS, t);
|
||||
}
|
||||
if (isConversationOrInboxRoute.value) {
|
||||
return getDefaultConversationHotKeys.value;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
return {
|
||||
conversationHotKeys,
|
||||
};
|
||||
}
|
||||
+47
-44
@@ -1,3 +1,8 @@
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { useRouter } from 'dashboard/composables/route';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
import {
|
||||
ICON_ACCOUNT_SETTINGS,
|
||||
ICON_AGENT_REPORTS,
|
||||
@@ -14,11 +19,9 @@ import {
|
||||
ICON_TEAM_REPORTS,
|
||||
ICON_USER_PROFILE,
|
||||
ICON_CONVERSATION_REPORTS,
|
||||
} from './CommandBarIcons';
|
||||
import { frontendURL } from '../../../helper/URLHelper';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
import { FEATURE_FLAGS } from '../../../featureFlags';
|
||||
} from 'dashboard/helper/commandbar/icons';
|
||||
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
|
||||
const GO_TO_COMMANDS = [
|
||||
{
|
||||
@@ -172,45 +175,45 @@ const GO_TO_COMMANDS = [
|
||||
},
|
||||
];
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const { isAdmin } = useAdmin();
|
||||
return {
|
||||
isAdmin,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountId: 'getCurrentAccountId',
|
||||
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||
}),
|
||||
goToCommandHotKeys() {
|
||||
let commands = GO_TO_COMMANDS.filter(cmd => {
|
||||
if (cmd.featureFlag) {
|
||||
return this.isFeatureEnabledonAccount(
|
||||
this.accountId,
|
||||
cmd.featureFlag
|
||||
);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
export function useGoToCommandHotKeys() {
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const { isAdmin } = useAdmin();
|
||||
|
||||
if (!this.isAdmin) {
|
||||
commands = commands.filter(command => command.role.includes('agent'));
|
||||
const currentAccountId = useMapGetter('getCurrentAccountId');
|
||||
const isFeatureEnabledOnAccount = useMapGetter(
|
||||
'accounts/isFeatureEnabledonAccount'
|
||||
);
|
||||
|
||||
const openRoute = url => {
|
||||
router.push(frontendURL(url));
|
||||
};
|
||||
|
||||
const goToCommandHotKeys = computed(() => {
|
||||
let commands = GO_TO_COMMANDS.filter(cmd => {
|
||||
if (cmd.featureFlag) {
|
||||
return isFeatureEnabledOnAccount.value(
|
||||
currentAccountId.value,
|
||||
cmd.featureFlag
|
||||
);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return commands.map(command => ({
|
||||
id: command.id,
|
||||
section: this.$t(command.section),
|
||||
title: this.$t(command.title),
|
||||
icon: command.icon,
|
||||
handler: () => this.openRoute(command.path(this.accountId)),
|
||||
}));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openRoute(url) {
|
||||
this.$router.push(frontendURL(url));
|
||||
},
|
||||
},
|
||||
};
|
||||
if (!isAdmin.value) {
|
||||
commands = commands.filter(command => command.role.includes('agent'));
|
||||
}
|
||||
|
||||
return commands.map(command => ({
|
||||
id: command.id,
|
||||
section: t(command.section),
|
||||
title: t(command.title),
|
||||
icon: command.icon,
|
||||
handler: () => openRoute(command.path(currentAccountId.value)),
|
||||
}));
|
||||
});
|
||||
|
||||
return {
|
||||
goToCommandHotKeys,
|
||||
};
|
||||
}
|
||||
+37
-31
@@ -1,13 +1,19 @@
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { useRoute } from 'dashboard/composables/route';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
|
||||
import { CMD_SNOOZE_NOTIFICATION } from './commandBarBusEvents';
|
||||
import { ICON_SNOOZE_NOTIFICATION } from './CommandBarIcons';
|
||||
import { CMD_SNOOZE_NOTIFICATION } from 'dashboard/helper/commandbar/events';
|
||||
import { ICON_SNOOZE_NOTIFICATION } from 'dashboard/helper/commandbar/icons';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
import { isAInboxViewRoute } from 'dashboard/helper/routeHelpers';
|
||||
|
||||
const SNOOZE_OPTIONS = wootConstants.SNOOZE_OPTIONS;
|
||||
|
||||
const createSnoozeHandler = option => () =>
|
||||
emitter.emit(CMD_SNOOZE_NOTIFICATION, option);
|
||||
|
||||
const INBOX_SNOOZE_EVENTS = [
|
||||
{
|
||||
id: 'snooze_notification',
|
||||
@@ -21,8 +27,7 @@ const INBOX_SNOOZE_EVENTS = [
|
||||
parent: 'snooze_notification',
|
||||
section: 'COMMAND_BAR.SECTIONS.SNOOZE_NOTIFICATION',
|
||||
icon: ICON_SNOOZE_NOTIFICATION,
|
||||
handler: () =>
|
||||
emitter.emit(CMD_SNOOZE_NOTIFICATION, SNOOZE_OPTIONS.AN_HOUR_FROM_NOW),
|
||||
handler: createSnoozeHandler(SNOOZE_OPTIONS.AN_HOUR_FROM_NOW),
|
||||
},
|
||||
{
|
||||
id: SNOOZE_OPTIONS.UNTIL_TOMORROW,
|
||||
@@ -30,8 +35,7 @@ const INBOX_SNOOZE_EVENTS = [
|
||||
section: 'COMMAND_BAR.SECTIONS.SNOOZE_NOTIFICATION',
|
||||
parent: 'snooze_notification',
|
||||
icon: ICON_SNOOZE_NOTIFICATION,
|
||||
handler: () =>
|
||||
emitter.emit(CMD_SNOOZE_NOTIFICATION, SNOOZE_OPTIONS.UNTIL_TOMORROW),
|
||||
handler: createSnoozeHandler(SNOOZE_OPTIONS.UNTIL_TOMORROW),
|
||||
},
|
||||
{
|
||||
id: SNOOZE_OPTIONS.UNTIL_NEXT_WEEK,
|
||||
@@ -39,8 +43,7 @@ const INBOX_SNOOZE_EVENTS = [
|
||||
section: 'COMMAND_BAR.SECTIONS.SNOOZE_NOTIFICATION',
|
||||
parent: 'snooze_notification',
|
||||
icon: ICON_SNOOZE_NOTIFICATION,
|
||||
handler: () =>
|
||||
emitter.emit(CMD_SNOOZE_NOTIFICATION, SNOOZE_OPTIONS.UNTIL_NEXT_WEEK),
|
||||
handler: createSnoozeHandler(SNOOZE_OPTIONS.UNTIL_NEXT_WEEK),
|
||||
},
|
||||
{
|
||||
id: SNOOZE_OPTIONS.UNTIL_NEXT_MONTH,
|
||||
@@ -48,8 +51,7 @@ const INBOX_SNOOZE_EVENTS = [
|
||||
section: 'COMMAND_BAR.SECTIONS.SNOOZE_NOTIFICATION',
|
||||
parent: 'snooze_notification',
|
||||
icon: ICON_SNOOZE_NOTIFICATION,
|
||||
handler: () =>
|
||||
emitter.emit(CMD_SNOOZE_NOTIFICATION, SNOOZE_OPTIONS.UNTIL_NEXT_MONTH),
|
||||
handler: createSnoozeHandler(SNOOZE_OPTIONS.UNTIL_NEXT_MONTH),
|
||||
},
|
||||
{
|
||||
id: SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME,
|
||||
@@ -57,26 +59,30 @@ const INBOX_SNOOZE_EVENTS = [
|
||||
section: 'COMMAND_BAR.SECTIONS.SNOOZE_NOTIFICATION',
|
||||
parent: 'snooze_notification',
|
||||
icon: ICON_SNOOZE_NOTIFICATION,
|
||||
handler: () =>
|
||||
emitter.emit(CMD_SNOOZE_NOTIFICATION, SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME),
|
||||
handler: createSnoozeHandler(SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME),
|
||||
},
|
||||
];
|
||||
export default {
|
||||
computed: {
|
||||
inboxHotKeys() {
|
||||
if (isAInboxViewRoute(this.$route.name)) {
|
||||
return this.prepareActions(INBOX_SNOOZE_EVENTS);
|
||||
}
|
||||
return [];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
prepareActions(actions) {
|
||||
return actions.map(action => ({
|
||||
...action,
|
||||
title: this.$t(action.title),
|
||||
section: this.$t(action.section),
|
||||
}));
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function useInboxHotKeys() {
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
|
||||
const prepareActions = actions => {
|
||||
return actions.map(action => ({
|
||||
...action,
|
||||
title: t(action.title),
|
||||
section: action.section ? t(action.section) : undefined,
|
||||
}));
|
||||
};
|
||||
|
||||
const inboxHotKeys = computed(() => {
|
||||
if (isAInboxViewRoute(route.name)) {
|
||||
return prepareActions(INBOX_SNOOZE_EVENTS);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
return {
|
||||
inboxHotKeys,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { allAgentsData } from 'dashboard/helper/specs/fixtures/agentFixtures';
|
||||
|
||||
export { allAgentsData };
|
||||
export const formattedAgentsData = [
|
||||
{
|
||||
account_id: 0,
|
||||
confirmed: true,
|
||||
email: 'None',
|
||||
id: 0,
|
||||
name: 'None',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'Abraham',
|
||||
confirmed: true,
|
||||
email: 'abraham@chatwoot.com',
|
||||
id: 5,
|
||||
name: 'Abraham Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'John K',
|
||||
confirmed: true,
|
||||
email: 'john@chatwoot.com',
|
||||
id: 1,
|
||||
name: 'John Kennady',
|
||||
role: 'administrator',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Honey',
|
||||
confirmed: true,
|
||||
email: 'bee@chatwoot.com',
|
||||
id: 4,
|
||||
name: 'Honey Bee',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Samuel K',
|
||||
confirmed: true,
|
||||
email: 'samuel@chatwoot.com',
|
||||
id: 2,
|
||||
name: 'Samuel Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
available_name: 'James K',
|
||||
confirmed: true,
|
||||
email: 'james@chatwoot.com',
|
||||
id: 3,
|
||||
name: 'James Koti',
|
||||
role: 'agent',
|
||||
},
|
||||
];
|
||||
@@ -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,119 @@
|
||||
import { useAI } from '../useAI';
|
||||
import {
|
||||
useStore,
|
||||
useStoreGetters,
|
||||
useMapGetter,
|
||||
} from 'dashboard/composables/store';
|
||||
import { useAlert, useTrack } from 'dashboard/composables';
|
||||
import { useI18n } from '../useI18n';
|
||||
import OpenAPI from 'dashboard/api/integrations/openapi';
|
||||
|
||||
vi.mock('dashboard/composables/store');
|
||||
vi.mock('dashboard/composables');
|
||||
vi.mock('../useI18n');
|
||||
vi.mock('dashboard/api/integrations/openapi');
|
||||
vi.mock('dashboard/helper/AnalyticsHelper/events', () => ({
|
||||
OPEN_AI_EVENTS: {
|
||||
TEST_EVENT: 'open_ai_test_event',
|
||||
},
|
||||
}));
|
||||
|
||||
describe('useAI', () => {
|
||||
const mockStore = {
|
||||
dispatch: vi.fn(),
|
||||
};
|
||||
|
||||
const mockGetters = {
|
||||
'integrations/getUIFlags': { value: { isFetching: false } },
|
||||
'draftMessages/get': { value: () => 'Draft message' },
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useStore.mockReturnValue(mockStore);
|
||||
useStoreGetters.mockReturnValue(mockGetters);
|
||||
useMapGetter.mockImplementation(getter => {
|
||||
const mockValues = {
|
||||
'integrations/getAppIntegrations': [],
|
||||
getSelectedChat: { id: '123' },
|
||||
'draftMessages/getReplyEditorMode': 'reply',
|
||||
};
|
||||
return { value: mockValues[getter] };
|
||||
});
|
||||
useTrack.mockReturnValue(vi.fn());
|
||||
useI18n.mockReturnValue({ t: vi.fn() });
|
||||
useAlert.mockReturnValue(vi.fn());
|
||||
});
|
||||
|
||||
it('initializes computed properties correctly', async () => {
|
||||
const { uiFlags, appIntegrations, currentChat, replyMode, draftMessage } =
|
||||
useAI();
|
||||
|
||||
expect(uiFlags.value).toEqual({ isFetching: false });
|
||||
expect(appIntegrations.value).toEqual([]);
|
||||
expect(currentChat.value).toEqual({ id: '123' });
|
||||
expect(replyMode.value).toBe('reply');
|
||||
expect(draftMessage.value).toBe('Draft message');
|
||||
});
|
||||
|
||||
it('fetches integrations if required', async () => {
|
||||
const { fetchIntegrationsIfRequired } = useAI();
|
||||
await fetchIntegrationsIfRequired();
|
||||
expect(mockStore.dispatch).toHaveBeenCalledWith('integrations/get');
|
||||
});
|
||||
|
||||
it('does not fetch integrations if already loaded', async () => {
|
||||
useMapGetter.mockImplementation(getter => {
|
||||
const mockValues = {
|
||||
'integrations/getAppIntegrations': [{ id: 'openai' }],
|
||||
getSelectedChat: { id: '123' },
|
||||
'draftMessages/getReplyEditorMode': 'reply',
|
||||
};
|
||||
return { value: mockValues[getter] };
|
||||
});
|
||||
|
||||
const { fetchIntegrationsIfRequired } = useAI();
|
||||
await fetchIntegrationsIfRequired();
|
||||
expect(mockStore.dispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('records analytics correctly', async () => {
|
||||
const mockTrack = vi.fn();
|
||||
useTrack.mockReturnValue(mockTrack);
|
||||
const { recordAnalytics } = useAI();
|
||||
|
||||
await recordAnalytics('TEST_EVENT', { data: 'test' });
|
||||
|
||||
expect(mockTrack).toHaveBeenCalledWith('open_ai_test_event', {
|
||||
type: 'TEST_EVENT',
|
||||
data: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
it('fetches label suggestions', async () => {
|
||||
OpenAPI.processEvent.mockResolvedValue({
|
||||
data: { message: 'label1, label2' },
|
||||
});
|
||||
|
||||
useMapGetter.mockImplementation(getter => {
|
||||
const mockValues = {
|
||||
'integrations/getAppIntegrations': [
|
||||
{ id: 'openai', hooks: [{ id: 'hook1' }] },
|
||||
],
|
||||
getSelectedChat: { id: '123' },
|
||||
};
|
||||
return { value: mockValues[getter] };
|
||||
});
|
||||
|
||||
const { fetchLabelSuggestions } = useAI();
|
||||
const result = await fetchLabelSuggestions();
|
||||
|
||||
expect(OpenAPI.processEvent).toHaveBeenCalledWith({
|
||||
type: 'label_suggestion',
|
||||
hookId: 'hook1',
|
||||
conversationId: '123',
|
||||
});
|
||||
|
||||
expect(result).toEqual(['label1', 'label2']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import { ref } from 'vue';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { useAgentsList } from '../useAgentsList';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { allAgentsData, formattedAgentsData } from './fixtures/agentFixtures';
|
||||
import * as agentHelper from 'dashboard/helper/agentHelper';
|
||||
|
||||
vi.mock('dashboard/composables/store');
|
||||
vi.mock('dashboard/helper/agentHelper');
|
||||
|
||||
const mockUseMapGetter = (overrides = {}) => {
|
||||
const defaultGetters = {
|
||||
getCurrentUser: ref(allAgentsData[0]),
|
||||
getSelectedChat: ref({ inbox_id: 1, meta: { assignee: true } }),
|
||||
getCurrentAccountId: ref(1),
|
||||
'inboxAssignableAgents/getAssignableAgents': ref(() => allAgentsData),
|
||||
};
|
||||
|
||||
const mergedGetters = { ...defaultGetters, ...overrides };
|
||||
|
||||
useMapGetter.mockImplementation(getter => mergedGetters[getter]);
|
||||
};
|
||||
|
||||
describe('useAgentsList', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
agentHelper.getAgentsByUpdatedPresence.mockImplementation(agents => agents);
|
||||
agentHelper.getSortedAgentsByAvailability.mockReturnValue(
|
||||
formattedAgentsData.slice(1)
|
||||
);
|
||||
agentHelper.getCombinedAgents.mockImplementation(
|
||||
(agents, includeNone, isAgentSelected) => {
|
||||
if (includeNone && isAgentSelected) {
|
||||
return [agentHelper.createNoneAgent, ...agents];
|
||||
}
|
||||
return agents;
|
||||
}
|
||||
);
|
||||
|
||||
mockUseMapGetter();
|
||||
});
|
||||
|
||||
it('returns agentsList and assignableAgents', () => {
|
||||
const { agentsList, assignableAgents } = useAgentsList();
|
||||
|
||||
expect(assignableAgents.value).toEqual(allAgentsData);
|
||||
expect(agentsList.value).toEqual([
|
||||
agentHelper.createNoneAgent,
|
||||
...formattedAgentsData.slice(1),
|
||||
]);
|
||||
});
|
||||
|
||||
it('includes None agent when includeNoneAgent is true', () => {
|
||||
const { agentsList } = useAgentsList(true);
|
||||
|
||||
expect(agentsList.value[0]).toEqual(agentHelper.createNoneAgent);
|
||||
expect(agentsList.value.length).toBe(formattedAgentsData.length);
|
||||
});
|
||||
|
||||
it('excludes None agent when includeNoneAgent is false', () => {
|
||||
const { agentsList } = useAgentsList(false);
|
||||
|
||||
expect(agentsList.value[0]).not.toEqual(agentHelper.createNoneAgent);
|
||||
expect(agentsList.value.length).toBe(formattedAgentsData.length - 1);
|
||||
});
|
||||
|
||||
it('handles empty assignable agents', () => {
|
||||
mockUseMapGetter({
|
||||
'inboxAssignableAgents/getAssignableAgents': ref(() => []),
|
||||
});
|
||||
agentHelper.getSortedAgentsByAvailability.mockReturnValue([]);
|
||||
|
||||
const { agentsList, assignableAgents } = useAgentsList();
|
||||
|
||||
expect(assignableAgents.value).toEqual([]);
|
||||
expect(agentsList.value).toEqual([agentHelper.createNoneAgent]);
|
||||
});
|
||||
|
||||
it('handles missing inbox_id', () => {
|
||||
mockUseMapGetter({
|
||||
getSelectedChat: ref({ meta: { assignee: true } }),
|
||||
'inboxAssignableAgents/getAssignableAgents': ref(() => []),
|
||||
});
|
||||
agentHelper.getSortedAgentsByAvailability.mockReturnValue([]);
|
||||
|
||||
const { agentsList, assignableAgents } = useAgentsList();
|
||||
|
||||
expect(assignableAgents.value).toEqual([]);
|
||||
expect(agentsList.value).toEqual([agentHelper.createNoneAgent]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,295 @@
|
||||
import { useAutomation } from '../useAutomation';
|
||||
import { useStoreGetters, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useI18n } from '../useI18n';
|
||||
import * as automationHelper from 'dashboard/helper/automationHelper';
|
||||
import {
|
||||
customAttributes,
|
||||
agents,
|
||||
teams,
|
||||
labels,
|
||||
statusFilterOptions,
|
||||
campaigns,
|
||||
contacts,
|
||||
inboxes,
|
||||
languages,
|
||||
countries,
|
||||
slaPolicies,
|
||||
} from 'dashboard/helper/specs/fixtures/automationFixtures.js';
|
||||
import { MESSAGE_CONDITION_VALUES } from 'dashboard/constants/automation';
|
||||
|
||||
vi.mock('dashboard/composables/store');
|
||||
vi.mock('dashboard/composables');
|
||||
vi.mock('../useI18n');
|
||||
vi.mock('dashboard/helper/automationHelper');
|
||||
|
||||
describe('useAutomation', () => {
|
||||
beforeEach(() => {
|
||||
useStoreGetters.mockReturnValue({
|
||||
'attributes/getAttributes': { value: customAttributes },
|
||||
'attributes/getAttributesByModel': {
|
||||
value: model => {
|
||||
return model === 'conversation_attribute'
|
||||
? [{ id: 1, name: 'Conversation Attribute' }]
|
||||
: [{ id: 2, name: 'Contact Attribute' }];
|
||||
},
|
||||
},
|
||||
});
|
||||
useMapGetter.mockImplementation(getter => {
|
||||
const getterMap = {
|
||||
'agents/getAgents': agents,
|
||||
'campaigns/getAllCampaigns': campaigns,
|
||||
'contacts/getContacts': contacts,
|
||||
'inboxes/getInboxes': inboxes,
|
||||
'labels/getLabels': labels,
|
||||
'teams/getTeams': teams,
|
||||
'sla/getSLA': slaPolicies,
|
||||
};
|
||||
return { value: getterMap[getter] };
|
||||
});
|
||||
useI18n.mockReturnValue({ t: key => key });
|
||||
useAlert.mockReturnValue(vi.fn());
|
||||
|
||||
// Mock getConditionOptions for different types
|
||||
automationHelper.getConditionOptions.mockImplementation(options => {
|
||||
const { type } = options;
|
||||
switch (type) {
|
||||
case 'status':
|
||||
return statusFilterOptions;
|
||||
case 'team_id':
|
||||
return teams;
|
||||
case 'assignee_id':
|
||||
return agents;
|
||||
case 'contact':
|
||||
return contacts;
|
||||
case 'inbox_id':
|
||||
return inboxes;
|
||||
case 'campaigns':
|
||||
return campaigns;
|
||||
case 'browser_language':
|
||||
return languages;
|
||||
case 'country_code':
|
||||
return countries;
|
||||
case 'message_type':
|
||||
return MESSAGE_CONDITION_VALUES;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
// Mock getActionOptions for different types
|
||||
automationHelper.getActionOptions.mockImplementation(options => {
|
||||
const { type } = options;
|
||||
switch (type) {
|
||||
case 'add_label':
|
||||
return labels;
|
||||
case 'assign_team':
|
||||
return teams;
|
||||
case 'assign_agent':
|
||||
return agents;
|
||||
case 'send_email_to_team':
|
||||
return teams;
|
||||
case 'send_message':
|
||||
return [];
|
||||
case 'add_sla':
|
||||
return slaPolicies;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('initializes computed properties correctly', () => {
|
||||
const {
|
||||
agents: computedAgents,
|
||||
campaigns: computedCampaigns,
|
||||
contacts: computedContacts,
|
||||
inboxes: computedInboxes,
|
||||
labels: computedLabels,
|
||||
teams: computedTeams,
|
||||
slaPolicies: computedSlaPolicies,
|
||||
} = useAutomation();
|
||||
|
||||
expect(computedAgents.value).toEqual(agents);
|
||||
expect(computedCampaigns.value).toEqual(campaigns);
|
||||
expect(computedContacts.value).toEqual(contacts);
|
||||
expect(computedInboxes.value).toEqual(inboxes);
|
||||
expect(computedLabels.value).toEqual(labels);
|
||||
expect(computedTeams.value).toEqual(teams);
|
||||
expect(computedSlaPolicies.value).toEqual(slaPolicies);
|
||||
});
|
||||
|
||||
it('appends new condition and action correctly', () => {
|
||||
const { appendNewCondition, appendNewAction } = useAutomation();
|
||||
const mockAutomation = {
|
||||
event_name: 'message_created',
|
||||
conditions: [],
|
||||
actions: [],
|
||||
};
|
||||
|
||||
automationHelper.getDefaultConditions.mockReturnValue([{}]);
|
||||
automationHelper.getDefaultActions.mockReturnValue([{}]);
|
||||
|
||||
appendNewCondition(mockAutomation);
|
||||
appendNewAction(mockAutomation);
|
||||
|
||||
expect(automationHelper.getDefaultConditions).toHaveBeenCalledWith(
|
||||
'message_created'
|
||||
);
|
||||
expect(automationHelper.getDefaultActions).toHaveBeenCalled();
|
||||
expect(mockAutomation.conditions).toHaveLength(1);
|
||||
expect(mockAutomation.actions).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('removes filter and action correctly', () => {
|
||||
const { removeFilter, removeAction } = useAutomation();
|
||||
const mockAutomation = {
|
||||
conditions: [{ id: 1 }, { id: 2 }],
|
||||
actions: [{ id: 1 }, { id: 2 }],
|
||||
};
|
||||
|
||||
removeFilter(mockAutomation, 0);
|
||||
removeAction(mockAutomation, 0);
|
||||
|
||||
expect(mockAutomation.conditions).toHaveLength(1);
|
||||
expect(mockAutomation.actions).toHaveLength(1);
|
||||
expect(mockAutomation.conditions[0].id).toBe(2);
|
||||
expect(mockAutomation.actions[0].id).toBe(2);
|
||||
});
|
||||
|
||||
it('resets filter and action correctly', () => {
|
||||
const { resetFilter, resetAction } = useAutomation();
|
||||
const mockAutomation = {
|
||||
event_name: 'message_created',
|
||||
conditions: [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: 'open',
|
||||
},
|
||||
],
|
||||
actions: [{ action_name: 'assign_agent', action_params: [1] }],
|
||||
};
|
||||
const mockAutomationTypes = {
|
||||
message_created: {
|
||||
conditions: [
|
||||
{ key: 'status', filterOperators: [{ value: 'not_equal_to' }] },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
resetFilter(
|
||||
mockAutomation,
|
||||
mockAutomationTypes,
|
||||
0,
|
||||
mockAutomation.conditions[0]
|
||||
);
|
||||
resetAction(mockAutomation, 0);
|
||||
|
||||
expect(mockAutomation.conditions[0].filter_operator).toBe('not_equal_to');
|
||||
expect(mockAutomation.conditions[0].values).toBe('');
|
||||
expect(mockAutomation.actions[0].action_params).toEqual([]);
|
||||
});
|
||||
|
||||
it('formats automation correctly', () => {
|
||||
const { formatAutomation } = useAutomation();
|
||||
const mockAutomation = {
|
||||
conditions: [{ attribute_key: 'status', values: ['open'] }],
|
||||
actions: [{ action_name: 'assign_agent', action_params: [1] }],
|
||||
};
|
||||
const mockAutomationTypes = {};
|
||||
const mockAutomationActionTypes = [
|
||||
{ key: 'assign_agent', inputType: 'search_select' },
|
||||
];
|
||||
|
||||
automationHelper.getConditionOptions.mockReturnValue([
|
||||
{ id: 'open', name: 'open' },
|
||||
]);
|
||||
automationHelper.getActionOptions.mockReturnValue([
|
||||
{ id: 1, name: 'Agent 1' },
|
||||
]);
|
||||
|
||||
const result = formatAutomation(
|
||||
mockAutomation,
|
||||
customAttributes,
|
||||
mockAutomationTypes,
|
||||
mockAutomationActionTypes
|
||||
);
|
||||
|
||||
expect(result.conditions[0].values).toEqual([{ id: 'open', name: 'open' }]);
|
||||
expect(result.actions[0].action_params).toEqual([
|
||||
{ id: 1, name: 'Agent 1' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('manifests custom attributes correctly', () => {
|
||||
const { manifestCustomAttributes } = useAutomation();
|
||||
const mockAutomationTypes = {
|
||||
message_created: { conditions: [] },
|
||||
conversation_created: { conditions: [] },
|
||||
conversation_updated: { conditions: [] },
|
||||
conversation_opened: { conditions: [] },
|
||||
};
|
||||
|
||||
automationHelper.generateCustomAttributeTypes.mockReturnValue([]);
|
||||
automationHelper.generateCustomAttributes.mockReturnValue([]);
|
||||
|
||||
manifestCustomAttributes(mockAutomationTypes);
|
||||
|
||||
expect(automationHelper.generateCustomAttributeTypes).toHaveBeenCalledTimes(
|
||||
2
|
||||
);
|
||||
expect(automationHelper.generateCustomAttributes).toHaveBeenCalledTimes(1);
|
||||
Object.values(mockAutomationTypes).forEach(type => {
|
||||
expect(type.conditions).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('gets condition dropdown values correctly', () => {
|
||||
const { getConditionDropdownValues } = useAutomation();
|
||||
|
||||
expect(getConditionDropdownValues('status')).toEqual(statusFilterOptions);
|
||||
expect(getConditionDropdownValues('team_id')).toEqual(teams);
|
||||
expect(getConditionDropdownValues('assignee_id')).toEqual(agents);
|
||||
expect(getConditionDropdownValues('contact')).toEqual(contacts);
|
||||
expect(getConditionDropdownValues('inbox_id')).toEqual(inboxes);
|
||||
expect(getConditionDropdownValues('campaigns')).toEqual(campaigns);
|
||||
expect(getConditionDropdownValues('browser_language')).toEqual(languages);
|
||||
expect(getConditionDropdownValues('country_code')).toEqual(countries);
|
||||
expect(getConditionDropdownValues('message_type')).toEqual(
|
||||
MESSAGE_CONDITION_VALUES
|
||||
);
|
||||
});
|
||||
|
||||
it('gets action dropdown values correctly', () => {
|
||||
const { getActionDropdownValues } = useAutomation();
|
||||
|
||||
expect(getActionDropdownValues('add_label')).toEqual(labels);
|
||||
expect(getActionDropdownValues('assign_team')).toEqual(teams);
|
||||
expect(getActionDropdownValues('assign_agent')).toEqual(agents);
|
||||
expect(getActionDropdownValues('send_email_to_team')).toEqual(teams);
|
||||
expect(getActionDropdownValues('send_message')).toEqual([]);
|
||||
expect(getActionDropdownValues('add_sla')).toEqual(slaPolicies);
|
||||
});
|
||||
|
||||
it('handles event change correctly', () => {
|
||||
const { onEventChange } = useAutomation();
|
||||
const mockAutomation = {
|
||||
event_name: 'message_created',
|
||||
conditions: [],
|
||||
actions: [],
|
||||
};
|
||||
|
||||
automationHelper.getDefaultConditions.mockReturnValue([{}]);
|
||||
automationHelper.getDefaultActions.mockReturnValue([{}]);
|
||||
|
||||
onEventChange(mockAutomation);
|
||||
|
||||
expect(automationHelper.getDefaultConditions).toHaveBeenCalledWith(
|
||||
'message_created'
|
||||
);
|
||||
expect(automationHelper.getDefaultActions).toHaveBeenCalled();
|
||||
expect(mockAutomation.conditions).toHaveLength(1);
|
||||
expect(mockAutomation.actions).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import { useIntegrationHook } from '../useIntegrationHook';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { nextTick } from 'vue';
|
||||
|
||||
vi.mock('dashboard/composables/store');
|
||||
|
||||
describe('useIntegrationHook', () => {
|
||||
let integrationGetter;
|
||||
|
||||
beforeEach(() => {
|
||||
integrationGetter = vi.fn();
|
||||
useMapGetter.mockReturnValue({ value: integrationGetter });
|
||||
});
|
||||
|
||||
it('should return the correct computed properties', async () => {
|
||||
const mockIntegration = {
|
||||
id: 1,
|
||||
hook_type: 'inbox',
|
||||
hooks: ['hook1', 'hook2'],
|
||||
allow_multiple_hooks: true,
|
||||
};
|
||||
integrationGetter.mockReturnValue(mockIntegration);
|
||||
|
||||
const hook = useIntegrationHook(1);
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(hook.integration.value).toEqual(mockIntegration);
|
||||
expect(hook.integrationType.value).toBe('multiple');
|
||||
expect(hook.isIntegrationMultiple.value).toBe(true);
|
||||
expect(hook.isIntegrationSingle.value).toBe(false);
|
||||
expect(hook.isHookTypeInbox.value).toBe(true);
|
||||
expect(hook.hasConnectedHooks.value).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle single integration type correctly', async () => {
|
||||
const mockIntegration = {
|
||||
id: 2,
|
||||
hook_type: 'channel',
|
||||
hooks: [],
|
||||
allow_multiple_hooks: false,
|
||||
};
|
||||
integrationGetter.mockReturnValue(mockIntegration);
|
||||
|
||||
const hook = useIntegrationHook(2);
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(hook.integration.value).toEqual(mockIntegration);
|
||||
expect(hook.integrationType.value).toBe('single');
|
||||
expect(hook.isIntegrationMultiple.value).toBe(false);
|
||||
expect(hook.isIntegrationSingle.value).toBe(true);
|
||||
expect(hook.isHookTypeInbox.value).toBe(false);
|
||||
expect(hook.hasConnectedHooks.value).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,3 @@
|
||||
import { unref } from 'vue';
|
||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
|
||||
describe('useKeyboardEvents', () => {
|
||||
@@ -11,15 +10,13 @@ describe('useKeyboardEvents', () => {
|
||||
});
|
||||
|
||||
it('should set up listeners on mount and remove them on unmount', async () => {
|
||||
const el = document.createElement('div');
|
||||
const elRef = unref({ value: el });
|
||||
const events = {
|
||||
'ALT+KeyL': () => {},
|
||||
};
|
||||
|
||||
const mountedMock = vi.fn();
|
||||
const unmountedMock = vi.fn();
|
||||
useKeyboardEvents(events, elRef);
|
||||
useKeyboardEvents(events);
|
||||
mountedMock();
|
||||
unmountedMock();
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ vi.mock('../useKeyboardEvents', () => ({
|
||||
}));
|
||||
|
||||
describe('useKeyboardNavigableList', () => {
|
||||
let elementRef;
|
||||
let items;
|
||||
let onSelect;
|
||||
let adjustScroll;
|
||||
@@ -18,7 +17,6 @@ describe('useKeyboardNavigableList', () => {
|
||||
const createMockEvent = () => ({ preventDefault: vi.fn() });
|
||||
|
||||
beforeEach(() => {
|
||||
elementRef = ref(null);
|
||||
items = ref(['item1', 'item2', 'item3']);
|
||||
onSelect = vi.fn();
|
||||
adjustScroll = vi.fn();
|
||||
@@ -28,7 +26,6 @@ describe('useKeyboardNavigableList', () => {
|
||||
|
||||
it('should return moveSelectionUp and moveSelectionDown functions', () => {
|
||||
const result = useKeyboardNavigableList({
|
||||
elementRef,
|
||||
items,
|
||||
onSelect,
|
||||
adjustScroll,
|
||||
@@ -43,7 +40,6 @@ describe('useKeyboardNavigableList', () => {
|
||||
|
||||
it('should move selection up correctly', () => {
|
||||
const { moveSelectionUp } = useKeyboardNavigableList({
|
||||
elementRef,
|
||||
items,
|
||||
onSelect,
|
||||
adjustScroll,
|
||||
@@ -65,7 +61,6 @@ describe('useKeyboardNavigableList', () => {
|
||||
|
||||
it('should move selection down correctly', () => {
|
||||
const { moveSelectionDown } = useKeyboardNavigableList({
|
||||
elementRef,
|
||||
items,
|
||||
onSelect,
|
||||
adjustScroll,
|
||||
@@ -87,7 +82,6 @@ describe('useKeyboardNavigableList', () => {
|
||||
|
||||
it('should call adjustScroll after moving selection', () => {
|
||||
const { moveSelectionUp, moveSelectionDown } = useKeyboardNavigableList({
|
||||
elementRef,
|
||||
items,
|
||||
onSelect,
|
||||
adjustScroll,
|
||||
@@ -103,7 +97,6 @@ describe('useKeyboardNavigableList', () => {
|
||||
|
||||
it('should include Enter key handler when onSelect is provided', () => {
|
||||
useKeyboardNavigableList({
|
||||
elementRef,
|
||||
items,
|
||||
onSelect,
|
||||
adjustScroll,
|
||||
@@ -118,7 +111,6 @@ describe('useKeyboardNavigableList', () => {
|
||||
|
||||
it('should not include Enter key handler when onSelect is not provided', () => {
|
||||
useKeyboardNavigableList({
|
||||
elementRef,
|
||||
items,
|
||||
adjustScroll,
|
||||
selectedIndex,
|
||||
@@ -131,7 +123,6 @@ describe('useKeyboardNavigableList', () => {
|
||||
|
||||
it('should not trigger onSelect when items are empty', () => {
|
||||
const { moveSelectionUp, moveSelectionDown } = useKeyboardNavigableList({
|
||||
elementRef,
|
||||
items: ref([]),
|
||||
onSelect,
|
||||
adjustScroll,
|
||||
@@ -145,23 +136,18 @@ describe('useKeyboardNavigableList', () => {
|
||||
|
||||
it('should call useKeyboardEvents with correct parameters', () => {
|
||||
useKeyboardNavigableList({
|
||||
elementRef,
|
||||
items,
|
||||
onSelect,
|
||||
adjustScroll,
|
||||
selectedIndex,
|
||||
});
|
||||
|
||||
expect(useKeyboardEvents).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
elementRef
|
||||
);
|
||||
expect(useKeyboardEvents).toHaveBeenCalledWith(expect.any(Object));
|
||||
});
|
||||
|
||||
// Keyboard event handlers
|
||||
it('should handle ArrowUp key', () => {
|
||||
useKeyboardNavigableList({
|
||||
elementRef,
|
||||
items,
|
||||
onSelect,
|
||||
adjustScroll,
|
||||
@@ -178,7 +164,6 @@ describe('useKeyboardNavigableList', () => {
|
||||
|
||||
it('should handle Control+KeyP', () => {
|
||||
useKeyboardNavigableList({
|
||||
elementRef,
|
||||
items,
|
||||
onSelect,
|
||||
adjustScroll,
|
||||
@@ -195,7 +180,6 @@ describe('useKeyboardNavigableList', () => {
|
||||
|
||||
it('should handle ArrowDown key', () => {
|
||||
useKeyboardNavigableList({
|
||||
elementRef,
|
||||
items,
|
||||
onSelect,
|
||||
adjustScroll,
|
||||
@@ -212,7 +196,6 @@ describe('useKeyboardNavigableList', () => {
|
||||
|
||||
it('should handle Control+KeyN', () => {
|
||||
useKeyboardNavigableList({
|
||||
elementRef,
|
||||
items,
|
||||
onSelect,
|
||||
adjustScroll,
|
||||
@@ -229,7 +212,6 @@ describe('useKeyboardNavigableList', () => {
|
||||
|
||||
it('should handle Enter key when onSelect is provided', () => {
|
||||
useKeyboardNavigableList({
|
||||
elementRef,
|
||||
items,
|
||||
onSelect,
|
||||
adjustScroll,
|
||||
@@ -245,7 +227,6 @@ describe('useKeyboardNavigableList', () => {
|
||||
|
||||
it('should not have Enter key handler when onSelect is not provided', () => {
|
||||
useKeyboardNavigableList({
|
||||
elementRef,
|
||||
items,
|
||||
adjustScroll,
|
||||
selectedIndex,
|
||||
@@ -257,7 +238,6 @@ describe('useKeyboardNavigableList', () => {
|
||||
|
||||
it('should set allowOnFocusedInput to true for all key handlers', () => {
|
||||
useKeyboardNavigableList({
|
||||
elementRef,
|
||||
items,
|
||||
onSelect,
|
||||
adjustScroll,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { useMacros } from '../useMacros';
|
||||
import { useStoreGetters } from 'dashboard/composables/store';
|
||||
import { PRIORITY_CONDITION_VALUES } from 'dashboard/helper/automationHelper.js';
|
||||
import { PRIORITY_CONDITION_VALUES } from 'dashboard/constants/automation';
|
||||
|
||||
vi.mock('dashboard/composables/store');
|
||||
vi.mock('dashboard/helper/automationHelper.js');
|
||||
|
||||
@@ -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,204 @@
|
||||
import { computed, onMounted } from 'vue';
|
||||
import {
|
||||
useStore,
|
||||
useStoreGetters,
|
||||
useMapGetter,
|
||||
} from 'dashboard/composables/store';
|
||||
import { useAlert, useTrack } from 'dashboard/composables';
|
||||
import { useI18n } from './useI18n';
|
||||
import { OPEN_AI_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import OpenAPI from 'dashboard/api/integrations/openapi';
|
||||
|
||||
/**
|
||||
* Cleans and normalizes a list of labels.
|
||||
* @param {string} labels - A comma-separated string of labels.
|
||||
* @returns {string[]} An array of cleaned and unique labels.
|
||||
*/
|
||||
const cleanLabels = labels => {
|
||||
return labels
|
||||
.toLowerCase() // Set it to lowercase
|
||||
.split(',') // split the string into an array
|
||||
.filter(label => label.trim()) // remove any empty strings
|
||||
.map(label => label.trim()) // trim the words
|
||||
.filter((label, index, self) => self.indexOf(label) === index);
|
||||
};
|
||||
|
||||
/**
|
||||
* A composable function for AI-related operations in the dashboard.
|
||||
* @returns {Object} An object containing AI-related methods and computed properties.
|
||||
*/
|
||||
export function useAI() {
|
||||
const store = useStore();
|
||||
const getters = useStoreGetters();
|
||||
const track = useTrack();
|
||||
const { t } = useI18n();
|
||||
|
||||
/**
|
||||
* Computed property for UI flags.
|
||||
* @type {import('vue').ComputedRef<Object>}
|
||||
*/
|
||||
const uiFlags = computed(() => getters['integrations/getUIFlags'].value);
|
||||
|
||||
const appIntegrations = useMapGetter('integrations/getAppIntegrations');
|
||||
const currentChat = useMapGetter('getSelectedChat');
|
||||
const replyMode = useMapGetter('draftMessages/getReplyEditorMode');
|
||||
|
||||
/**
|
||||
* Computed property for the AI integration.
|
||||
* @type {import('vue').ComputedRef<Object|undefined>}
|
||||
*/
|
||||
const aiIntegration = computed(
|
||||
() =>
|
||||
appIntegrations.value.find(
|
||||
integration => integration.id === 'openai' && !!integration.hooks.length
|
||||
)?.hooks[0]
|
||||
);
|
||||
|
||||
/**
|
||||
* Computed property to check if AI integration is enabled.
|
||||
* @type {import('vue').ComputedRef<boolean>}
|
||||
*/
|
||||
const isAIIntegrationEnabled = computed(() => !!aiIntegration.value);
|
||||
|
||||
/**
|
||||
* Computed property to check if label suggestion feature is enabled.
|
||||
* @type {import('vue').ComputedRef<boolean>}
|
||||
*/
|
||||
const isLabelSuggestionFeatureEnabled = computed(() => {
|
||||
if (aiIntegration.value) {
|
||||
const { settings = {} } = aiIntegration.value || {};
|
||||
return settings.label_suggestion;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
/**
|
||||
* Computed property to check if app integrations are being fetched.
|
||||
* @type {import('vue').ComputedRef<boolean>}
|
||||
*/
|
||||
const isFetchingAppIntegrations = computed(() => uiFlags.value.isFetching);
|
||||
|
||||
/**
|
||||
* Computed property for the hook ID.
|
||||
* @type {import('vue').ComputedRef<string|undefined>}
|
||||
*/
|
||||
const hookId = computed(() => aiIntegration.value?.id);
|
||||
|
||||
/**
|
||||
* Computed property for the conversation ID.
|
||||
* @type {import('vue').ComputedRef<string|undefined>}
|
||||
*/
|
||||
const conversationId = computed(() => currentChat.value?.id);
|
||||
|
||||
/**
|
||||
* Computed property for the draft key.
|
||||
* @type {import('vue').ComputedRef<string>}
|
||||
*/
|
||||
const draftKey = computed(
|
||||
() => `draft-${conversationId.value}-${replyMode.value}`
|
||||
);
|
||||
|
||||
/**
|
||||
* Computed property for the draft message.
|
||||
* @type {import('vue').ComputedRef<string>}
|
||||
*/
|
||||
const draftMessage = computed(() =>
|
||||
getters['draftMessages/get'].value(draftKey.value)
|
||||
);
|
||||
|
||||
/**
|
||||
* Fetches integrations if they haven't been loaded yet.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const fetchIntegrationsIfRequired = async () => {
|
||||
if (!appIntegrations.value.length) {
|
||||
await store.dispatch('integrations/get');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Records analytics for AI-related events.
|
||||
* @param {string} type - The type of event.
|
||||
* @param {Object} payload - Additional data for the event.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const recordAnalytics = async (type, payload) => {
|
||||
const event = OPEN_AI_EVENTS[type.toUpperCase()];
|
||||
if (event) {
|
||||
track(event, {
|
||||
type,
|
||||
...payload,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches label suggestions for the current conversation.
|
||||
* @returns {Promise<string[]>} An array of suggested labels.
|
||||
*/
|
||||
const fetchLabelSuggestions = async () => {
|
||||
if (!conversationId.value) return [];
|
||||
|
||||
try {
|
||||
const result = await OpenAPI.processEvent({
|
||||
type: 'label_suggestion',
|
||||
hookId: hookId.value,
|
||||
conversationId: conversationId.value,
|
||||
});
|
||||
|
||||
const {
|
||||
data: { message: labels },
|
||||
} = result;
|
||||
|
||||
return cleanLabels(labels);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Processes an AI event, such as rephrasing content.
|
||||
* @param {string} [type='rephrase'] - The type of AI event to process.
|
||||
* @returns {Promise<string>} The generated message or an empty string if an error occurs.
|
||||
*/
|
||||
const processEvent = async (type = 'rephrase') => {
|
||||
try {
|
||||
const result = await OpenAPI.processEvent({
|
||||
hookId: hookId.value,
|
||||
type,
|
||||
content: draftMessage.value,
|
||||
conversationId: conversationId.value,
|
||||
});
|
||||
const {
|
||||
data: { message: generatedMessage },
|
||||
} = result;
|
||||
return generatedMessage;
|
||||
} catch (error) {
|
||||
const errorData = error.response.data.error;
|
||||
const errorMessage =
|
||||
errorData?.error?.message ||
|
||||
t('INTEGRATION_SETTINGS.OPEN_AI.GENERATE_ERROR');
|
||||
useAlert(errorMessage);
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchIntegrationsIfRequired();
|
||||
});
|
||||
|
||||
return {
|
||||
draftMessage,
|
||||
uiFlags,
|
||||
appIntegrations,
|
||||
currentChat,
|
||||
replyMode,
|
||||
isAIIntegrationEnabled,
|
||||
isLabelSuggestionFeatureEnabled,
|
||||
isFetchingAppIntegrations,
|
||||
fetchIntegrationsIfRequired,
|
||||
recordAnalytics,
|
||||
fetchLabelSuggestions,
|
||||
processEvent,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { computed } from 'vue';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import {
|
||||
getAgentsByUpdatedPresence,
|
||||
getSortedAgentsByAvailability,
|
||||
getCombinedAgents,
|
||||
} from 'dashboard/helper/agentHelper';
|
||||
|
||||
/**
|
||||
* A composable function that provides a list of agents for assignment.
|
||||
*
|
||||
* @param {boolean} [includeNoneAgent=true] - Whether to include a 'None' agent option.
|
||||
* @returns {Object} An object containing the agents list and assignable agents.
|
||||
*/
|
||||
export function useAgentsList(includeNoneAgent = true) {
|
||||
const currentUser = useMapGetter('getCurrentUser');
|
||||
const currentChat = useMapGetter('getSelectedChat');
|
||||
const currentAccountId = useMapGetter('getCurrentAccountId');
|
||||
const assignable = useMapGetter('inboxAssignableAgents/getAssignableAgents');
|
||||
|
||||
const inboxId = computed(() => currentChat.value?.inbox_id);
|
||||
const isAgentSelected = computed(() => currentChat.value?.meta?.assignee);
|
||||
|
||||
/**
|
||||
* @type {import('vue').ComputedRef<Array>}
|
||||
*/
|
||||
const assignableAgents = computed(() => {
|
||||
return inboxId.value ? assignable.value(inboxId.value) : [];
|
||||
});
|
||||
|
||||
/**
|
||||
* @type {import('vue').ComputedRef<Array>}
|
||||
*/
|
||||
const agentsList = computed(() => {
|
||||
const agents = assignableAgents.value || [];
|
||||
const agentsByUpdatedPresence = getAgentsByUpdatedPresence(
|
||||
agents,
|
||||
currentUser.value,
|
||||
currentAccountId.value
|
||||
);
|
||||
|
||||
const filteredAgentsByAvailability = getSortedAgentsByAvailability(
|
||||
agentsByUpdatedPresence
|
||||
);
|
||||
|
||||
return getCombinedAgents(
|
||||
filteredAgentsByAvailability,
|
||||
includeNoneAgent,
|
||||
isAgentSelected.value
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
agentsList,
|
||||
assignableAgents,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
import { computed } from 'vue';
|
||||
import { useStoreGetters, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useI18n } from './useI18n';
|
||||
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||
import countries from 'shared/constants/countries';
|
||||
import {
|
||||
generateCustomAttributeTypes,
|
||||
getActionOptions,
|
||||
getConditionOptions,
|
||||
getCustomAttributeInputType,
|
||||
getDefaultConditions,
|
||||
getDefaultActions,
|
||||
filterCustomAttributes,
|
||||
getStandardAttributeInputType,
|
||||
isCustomAttribute,
|
||||
generateCustomAttributes,
|
||||
} from 'dashboard/helper/automationHelper';
|
||||
|
||||
/**
|
||||
* Composable for handling automation-related functionality.
|
||||
* @returns {Object} An object containing various automation-related functions and computed properties.
|
||||
*/
|
||||
export function useAutomation() {
|
||||
const getters = useStoreGetters();
|
||||
const { t } = useI18n();
|
||||
|
||||
const agents = useMapGetter('agents/getAgents');
|
||||
const campaigns = useMapGetter('campaigns/getAllCampaigns');
|
||||
const contacts = useMapGetter('contacts/getContacts');
|
||||
const inboxes = useMapGetter('inboxes/getInboxes');
|
||||
const labels = useMapGetter('labels/getLabels');
|
||||
const teams = useMapGetter('teams/getTeams');
|
||||
const slaPolicies = useMapGetter('sla/getSLA');
|
||||
|
||||
const booleanFilterOptions = computed(() => [
|
||||
{ id: true, name: t('FILTER.ATTRIBUTE_LABELS.TRUE') },
|
||||
{ id: false, name: t('FILTER.ATTRIBUTE_LABELS.FALSE') },
|
||||
]);
|
||||
|
||||
const statusFilterOptions = computed(() => {
|
||||
const statusFilters = t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS');
|
||||
return [
|
||||
...Object.keys(statusFilters).map(status => ({
|
||||
id: status,
|
||||
name: statusFilters[status].TEXT,
|
||||
})),
|
||||
{ id: 'all', name: t('CHAT_LIST.FILTER_ALL') },
|
||||
];
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles the event change for an automation.
|
||||
* @param {Object} automation - The automation object to update.
|
||||
*/
|
||||
const onEventChange = automation => {
|
||||
automation.conditions = getDefaultConditions(automation.event_name);
|
||||
automation.actions = getDefaultActions();
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the condition dropdown values for a given type.
|
||||
* @param {string} type - The type of condition.
|
||||
* @returns {Array} An array of condition dropdown values.
|
||||
*/
|
||||
const getConditionDropdownValues = type => {
|
||||
return getConditionOptions({
|
||||
agents: agents.value,
|
||||
booleanFilterOptions: booleanFilterOptions.value,
|
||||
campaigns: campaigns.value,
|
||||
contacts: contacts.value,
|
||||
customAttributes: getters['attributes/getAttributes'].value,
|
||||
inboxes: inboxes.value,
|
||||
statusFilterOptions: statusFilterOptions.value,
|
||||
teams: teams.value,
|
||||
languages,
|
||||
countries,
|
||||
type,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Appends a new condition to the automation.
|
||||
* @param {Object} automation - The automation object to update.
|
||||
*/
|
||||
const appendNewCondition = automation => {
|
||||
automation.conditions.push(...getDefaultConditions(automation.event_name));
|
||||
};
|
||||
|
||||
/**
|
||||
* Appends a new action to the automation.
|
||||
* @param {Object} automation - The automation object to update.
|
||||
*/
|
||||
const appendNewAction = automation => {
|
||||
automation.actions.push(...getDefaultActions());
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes a filter from the automation.
|
||||
* @param {Object} automation - The automation object to update.
|
||||
* @param {number} index - The index of the filter to remove.
|
||||
*/
|
||||
const removeFilter = (automation, index) => {
|
||||
if (automation.conditions.length <= 1) {
|
||||
useAlert(t('AUTOMATION.CONDITION.DELETE_MESSAGE'));
|
||||
} else {
|
||||
automation.conditions.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes an action from the automation.
|
||||
* @param {Object} automation - The automation object to update.
|
||||
* @param {number} index - The index of the action to remove.
|
||||
*/
|
||||
const removeAction = (automation, index) => {
|
||||
if (automation.actions.length <= 1) {
|
||||
useAlert(t('AUTOMATION.ACTION.DELETE_MESSAGE'));
|
||||
} else {
|
||||
automation.actions.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Resets a filter in the automation.
|
||||
* @param {Object} automation - The automation object to update.
|
||||
* @param {Object} automationTypes - The automation types object.
|
||||
* @param {number} index - The index of the filter to reset.
|
||||
* @param {Object} currentCondition - The current condition object.
|
||||
*/
|
||||
const resetFilter = (
|
||||
automation,
|
||||
automationTypes,
|
||||
index,
|
||||
currentCondition
|
||||
) => {
|
||||
automation.conditions[index].filter_operator = automationTypes[
|
||||
automation.event_name
|
||||
].conditions.find(
|
||||
condition => condition.key === currentCondition.attribute_key
|
||||
).filterOperators[0].value;
|
||||
automation.conditions[index].values = '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Resets an action in the automation.
|
||||
* @param {Object} automation - The automation object to update.
|
||||
* @param {number} index - The index of the action to reset.
|
||||
*/
|
||||
const resetAction = (automation, index) => {
|
||||
automation.actions[index].action_params = [];
|
||||
};
|
||||
|
||||
/**
|
||||
* This function sets the conditions for automation.
|
||||
* It help to format the conditions for the automation when we open the edit automation modal.
|
||||
* @param {Object} automation - The automation object containing conditions to manifest.
|
||||
* @param {Array} allCustomAttributes - List of all custom attributes.
|
||||
* @param {Object} automationTypes - Object containing automation type definitions.
|
||||
* @returns {Array} An array of manifested conditions.
|
||||
*/
|
||||
const manifestConditions = (
|
||||
automation,
|
||||
allCustomAttributes,
|
||||
automationTypes
|
||||
) => {
|
||||
const customAttributes = filterCustomAttributes(allCustomAttributes);
|
||||
return automation.conditions.map(condition => {
|
||||
const customAttr = isCustomAttribute(
|
||||
customAttributes,
|
||||
condition.attribute_key
|
||||
);
|
||||
let inputType = 'plain_text';
|
||||
if (customAttr) {
|
||||
inputType = getCustomAttributeInputType(customAttr.type);
|
||||
} else {
|
||||
inputType = getStandardAttributeInputType(
|
||||
automationTypes,
|
||||
automation.event_name,
|
||||
condition.attribute_key
|
||||
);
|
||||
}
|
||||
if (inputType === 'plain_text' || inputType === 'date') {
|
||||
return { ...condition, values: condition.values[0] };
|
||||
}
|
||||
if (inputType === 'comma_separated_plain_text') {
|
||||
return { ...condition, values: condition.values.join(',') };
|
||||
}
|
||||
return {
|
||||
...condition,
|
||||
query_operator: condition.query_operator || 'and',
|
||||
values: [...getConditionDropdownValues(condition.attribute_key)].filter(
|
||||
item => [...condition.values].includes(item.id)
|
||||
),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the action dropdown values for a given type.
|
||||
* @param {string} type - The type of action.
|
||||
* @returns {Array} An array of action dropdown values.
|
||||
*/
|
||||
const getActionDropdownValues = type => {
|
||||
return getActionOptions({
|
||||
agents: agents.value,
|
||||
labels: labels.value,
|
||||
teams: teams.value,
|
||||
slaPolicies: slaPolicies.value,
|
||||
languages,
|
||||
type,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates an array of actions for the automation.
|
||||
* @param {Object} action - The action object.
|
||||
* @param {Array} automationActionTypes - List of available automation action types.
|
||||
* @returns {Array|Object} Generated actions array or object based on input type.
|
||||
*/
|
||||
const generateActionsArray = (action, automationActionTypes) => {
|
||||
const params = action.action_params;
|
||||
const inputType = automationActionTypes.find(
|
||||
item => item.key === action.action_name
|
||||
).inputType;
|
||||
if (inputType === 'multi_select' || inputType === 'search_select') {
|
||||
return [...getActionDropdownValues(action.action_name)].filter(item =>
|
||||
[...params].includes(item.id)
|
||||
);
|
||||
}
|
||||
if (inputType === 'team_message') {
|
||||
return {
|
||||
team_ids: [...getActionDropdownValues(action.action_name)].filter(
|
||||
item => [...params[0].team_ids].includes(item.id)
|
||||
),
|
||||
message: params[0].message,
|
||||
};
|
||||
}
|
||||
return [...params];
|
||||
};
|
||||
|
||||
/**
|
||||
* This function sets the actions for automation.
|
||||
* It help to format the actions for the automation when we open the edit automation modal.
|
||||
* @param {Object} automation - The automation object containing actions.
|
||||
* @param {Array} automationActionTypes - List of available automation action types.
|
||||
* @returns {Array} An array of manifested actions.
|
||||
*/
|
||||
const manifestActions = (automation, automationActionTypes) => {
|
||||
return automation.actions.map(action => ({
|
||||
...action,
|
||||
action_params: action.action_params.length
|
||||
? generateActionsArray(action, automationActionTypes)
|
||||
: [],
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats the automation object for use when we edit the automation.
|
||||
* It help to format the conditions and actions for the automation when we open the edit automation modal.
|
||||
* @param {Object} automation - The automation object to format.
|
||||
* @param {Array} allCustomAttributes - List of all custom attributes.
|
||||
* @param {Object} automationTypes - Object containing automation type definitions.
|
||||
* @param {Array} automationActionTypes - List of available automation action types.
|
||||
* @returns {Object} A new object with formatted automation data, including automation conditions and actions.
|
||||
*/
|
||||
const formatAutomation = (
|
||||
automation,
|
||||
allCustomAttributes,
|
||||
automationTypes,
|
||||
automationActionTypes
|
||||
) => {
|
||||
return {
|
||||
...automation,
|
||||
conditions: manifestConditions(
|
||||
automation,
|
||||
allCustomAttributes,
|
||||
automationTypes
|
||||
),
|
||||
actions: manifestActions(automation, automationActionTypes),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This function formats the custom attributes for automation types.
|
||||
* It retrieves custom attributes for conversations and contacts,
|
||||
* generates custom attribute types, and adds them to the relevant automation types.
|
||||
* @param {Object} automationTypes - The automation types object to update with custom attributes.
|
||||
*/
|
||||
const manifestCustomAttributes = automationTypes => {
|
||||
const conversationCustomAttributesRaw = getters[
|
||||
'attributes/getAttributesByModel'
|
||||
].value('conversation_attribute');
|
||||
const contactCustomAttributesRaw =
|
||||
getters['attributes/getAttributesByModel'].value('contact_attribute');
|
||||
|
||||
const conversationCustomAttributeTypes = generateCustomAttributeTypes(
|
||||
conversationCustomAttributesRaw,
|
||||
'conversation_attribute'
|
||||
);
|
||||
const contactCustomAttributeTypes = generateCustomAttributeTypes(
|
||||
contactCustomAttributesRaw,
|
||||
'contact_attribute'
|
||||
);
|
||||
|
||||
const manifestedCustomAttributes = generateCustomAttributes(
|
||||
conversationCustomAttributeTypes,
|
||||
contactCustomAttributeTypes,
|
||||
t('AUTOMATION.CONDITION.CONVERSATION_CUSTOM_ATTR_LABEL'),
|
||||
t('AUTOMATION.CONDITION.CONTACT_CUSTOM_ATTR_LABEL')
|
||||
);
|
||||
|
||||
automationTypes.message_created.conditions.push(
|
||||
...manifestedCustomAttributes
|
||||
);
|
||||
automationTypes.conversation_created.conditions.push(
|
||||
...manifestedCustomAttributes
|
||||
);
|
||||
automationTypes.conversation_updated.conditions.push(
|
||||
...manifestedCustomAttributes
|
||||
);
|
||||
automationTypes.conversation_opened.conditions.push(
|
||||
...manifestedCustomAttributes
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
agents,
|
||||
campaigns,
|
||||
contacts,
|
||||
inboxes,
|
||||
labels,
|
||||
teams,
|
||||
slaPolicies,
|
||||
booleanFilterOptions,
|
||||
statusFilterOptions,
|
||||
onEventChange,
|
||||
getConditionDropdownValues,
|
||||
appendNewCondition,
|
||||
appendNewAction,
|
||||
removeFilter,
|
||||
removeAction,
|
||||
resetFilter,
|
||||
resetAction,
|
||||
formatAutomation,
|
||||
getActionDropdownValues,
|
||||
manifestCustomAttributes,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { computed } from 'vue';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
|
||||
/**
|
||||
* Composable for managing integration hooks
|
||||
* @param {string|number} integrationId - The ID of the integration
|
||||
* @returns {Object} An object containing computed properties for the integration
|
||||
*/
|
||||
export const useIntegrationHook = integrationId => {
|
||||
const integrationGetter = useMapGetter('integrations/getIntegration');
|
||||
|
||||
/**
|
||||
* The integration object
|
||||
* @type {import('vue').ComputedRef<Object>}
|
||||
*/
|
||||
const integration = computed(() => {
|
||||
return integrationGetter.value(integrationId);
|
||||
});
|
||||
|
||||
/**
|
||||
* Whether the integration hook type is 'inbox'
|
||||
* @type {import('vue').ComputedRef<boolean>}
|
||||
*/
|
||||
const isHookTypeInbox = computed(() => {
|
||||
return integration.value.hook_type === 'inbox';
|
||||
});
|
||||
|
||||
/**
|
||||
* Whether the integration has any connected hooks
|
||||
* @type {import('vue').ComputedRef<boolean>}
|
||||
*/
|
||||
const hasConnectedHooks = computed(() => {
|
||||
return !!integration.value.hooks.length;
|
||||
});
|
||||
|
||||
/**
|
||||
* The type of integration: 'multiple' or 'single'
|
||||
* @type {import('vue').ComputedRef<string>}
|
||||
*/
|
||||
const integrationType = computed(() => {
|
||||
return integration.value.allow_multiple_hooks ? 'multiple' : 'single';
|
||||
});
|
||||
|
||||
/**
|
||||
* Whether the integration allows multiple hooks
|
||||
* @type {import('vue').ComputedRef<boolean>}
|
||||
*/
|
||||
const isIntegrationMultiple = computed(() => {
|
||||
return integrationType.value === 'multiple';
|
||||
});
|
||||
|
||||
/**
|
||||
* Whether the integration allows only a single hook
|
||||
* @type {import('vue').ComputedRef<boolean>}
|
||||
*/
|
||||
const isIntegrationSingle = computed(() => {
|
||||
return integrationType.value === 'single';
|
||||
});
|
||||
|
||||
return {
|
||||
integration,
|
||||
integrationType,
|
||||
isIntegrationMultiple,
|
||||
isIntegrationSingle,
|
||||
isHookTypeInbox,
|
||||
hasConnectedHooks,
|
||||
};
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
import { onMounted, onBeforeUnmount, unref } from 'vue';
|
||||
import {
|
||||
isActiveElementTypeable,
|
||||
isEscape,
|
||||
@@ -7,8 +6,7 @@ import {
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import { useDetectKeyboardLayout } from 'dashboard/composables/useDetectKeyboardLayout';
|
||||
import { createKeybindingsHandler } from 'tinykeys';
|
||||
|
||||
const keyboardListenerMap = new WeakMap();
|
||||
import { onUnmounted, onMounted } from 'vue';
|
||||
|
||||
/**
|
||||
* Determines if the keyboard event should be ignored based on the element type and handler settings.
|
||||
@@ -69,49 +67,24 @@ async function wrapEventsInKeybindingsHandler(events) {
|
||||
return wrappedEvents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up keyboard event listeners on the specified element.
|
||||
* @param {Element} root - The DOM element to attach listeners to.
|
||||
* @param {Object} events - The events to listen for.
|
||||
*/
|
||||
const setupListeners = (root, events) => {
|
||||
if (root instanceof Element && events) {
|
||||
const keydownHandler = createKeybindingsHandler(events);
|
||||
document.addEventListener('keydown', keydownHandler);
|
||||
keyboardListenerMap.set(root, keydownHandler);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes keyboard event listeners from the specified element.
|
||||
* @param {Element} root - The DOM element to remove listeners from.
|
||||
*/
|
||||
const removeListeners = root => {
|
||||
// In the future, let's use the abort controller to remove the listeners
|
||||
// https://caniuse.com/abortcontroller
|
||||
if (root instanceof Element) {
|
||||
const handlerToRemove = keyboardListenerMap.get(root);
|
||||
document.removeEventListener('keydown', handlerToRemove);
|
||||
keyboardListenerMap.delete(root);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Vue composable to handle keyboard events with support for different keyboard layouts.
|
||||
* @param {Object} keyboardEvents - The keyboard events to handle.
|
||||
* @param {ref} elRef - A Vue ref to the element to attach the keyboard events to.
|
||||
*/
|
||||
export function useKeyboardEvents(keyboardEvents, elRef = null) {
|
||||
export async function useKeyboardEvents(keyboardEvents) {
|
||||
let abortController = new AbortController();
|
||||
|
||||
onMounted(async () => {
|
||||
const el = unref(elRef);
|
||||
const getKeyboardEvents = () => keyboardEvents || null;
|
||||
const events = getKeyboardEvents();
|
||||
const wrappedEvents = await wrapEventsInKeybindingsHandler(events);
|
||||
setupListeners(el, wrappedEvents);
|
||||
if (!keyboardEvents) return;
|
||||
const wrappedEvents = await wrapEventsInKeybindingsHandler(keyboardEvents);
|
||||
const keydownHandler = createKeybindingsHandler(wrappedEvents);
|
||||
|
||||
document.addEventListener('keydown', keydownHandler, {
|
||||
signal: abortController.signal,
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
const el = unref(elRef);
|
||||
removeListeners(el);
|
||||
onUnmounted(() => {
|
||||
abortController.abort();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -84,7 +84,6 @@ const updateSelectionIndex = (currentIndex, itemsLength, direction) => {
|
||||
* }} An object containing functions to move the selection up and down.
|
||||
*/
|
||||
export function useKeyboardNavigableList({
|
||||
elementRef,
|
||||
items,
|
||||
onSelect,
|
||||
adjustScroll,
|
||||
@@ -109,7 +108,7 @@ export function useKeyboardNavigableList({
|
||||
items
|
||||
);
|
||||
|
||||
useKeyboardEvents(keyboardEvents, elementRef);
|
||||
useKeyboardEvents(keyboardEvents);
|
||||
|
||||
return {
|
||||
moveSelectionUp,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { computed } from 'vue';
|
||||
import { useStoreGetters } from 'dashboard/composables/store';
|
||||
import { PRIORITY_CONDITION_VALUES } from 'dashboard/helper/automationHelper.js';
|
||||
import { PRIORITY_CONDITION_VALUES } from 'dashboard/constants/automation';
|
||||
|
||||
/**
|
||||
* Composable for handling macro-related functionality
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
export const DEFAULT_MESSAGE_CREATED_CONDITION = [
|
||||
{
|
||||
attribute_key: 'message_type',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_CONVERSATION_OPENED_CONDITION = [
|
||||
{
|
||||
attribute_key: 'browser_language',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_OTHER_CONDITION = [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_ACTIONS = [
|
||||
{
|
||||
action_name: 'assign_agent',
|
||||
action_params: [],
|
||||
},
|
||||
];
|
||||
|
||||
export const MESSAGE_CONDITION_VALUES = [
|
||||
{
|
||||
id: 'incoming',
|
||||
name: 'Incoming Message',
|
||||
},
|
||||
{
|
||||
id: 'outgoing',
|
||||
name: 'Outgoing Message',
|
||||
},
|
||||
];
|
||||
|
||||
export const PRIORITY_CONDITION_VALUES = [
|
||||
{
|
||||
id: 'nil',
|
||||
name: 'None',
|
||||
},
|
||||
{
|
||||
id: 'low',
|
||||
name: 'Low',
|
||||
},
|
||||
{
|
||||
id: 'medium',
|
||||
name: 'Medium',
|
||||
},
|
||||
{
|
||||
id: 'high',
|
||||
name: 'High',
|
||||
},
|
||||
{
|
||||
id: 'urgent',
|
||||
name: 'Urgent',
|
||||
},
|
||||
];
|
||||
@@ -36,6 +36,22 @@ class DashboardAudioNotificationHelper {
|
||||
initFaviconSwitcher();
|
||||
};
|
||||
|
||||
updateInstanceValues = ({
|
||||
alwaysPlayAudioAlert,
|
||||
alertIfUnreadConversationExist,
|
||||
audioAlertType,
|
||||
audioAlertTone,
|
||||
}) => {
|
||||
this.audioAlertType = audioAlertType;
|
||||
this.playAlertOnlyWhenHidden = !alwaysPlayAudioAlert;
|
||||
this.alertIfUnreadConversationExist = alertIfUnreadConversationExist;
|
||||
this.audioAlertTone = audioAlertTone;
|
||||
getAlertAudio('', {
|
||||
type: 'dashboard',
|
||||
alertTone: this.audioAlertTone,
|
||||
});
|
||||
};
|
||||
|
||||
onAudioListenEvent = async () => {
|
||||
try {
|
||||
await getAlertAudio('', {
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Default agent object representing 'None'
|
||||
* @type {Object}
|
||||
*/
|
||||
export const createNoneAgent = {
|
||||
confirmed: true,
|
||||
name: 'None',
|
||||
id: 0,
|
||||
role: 'agent',
|
||||
account_id: 0,
|
||||
email: 'None',
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters and sorts agents by availability status
|
||||
* @param {Array} agents - List of agents
|
||||
* @param {string} availability - Availability status to filter by
|
||||
* @returns {Array} Filtered and sorted list of agents
|
||||
*/
|
||||
export const getAgentsByAvailability = (agents, availability) => {
|
||||
return agents
|
||||
.filter(agent => agent.availability_status === availability)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
/**
|
||||
* Sorts agents by availability status: online, busy, then offline
|
||||
* @param {Array} agents - List of agents
|
||||
* @returns {Array} Sorted list of agents
|
||||
*/
|
||||
export const getSortedAgentsByAvailability = agents => {
|
||||
const onlineAgents = getAgentsByAvailability(agents, 'online');
|
||||
const busyAgents = getAgentsByAvailability(agents, 'busy');
|
||||
const offlineAgents = getAgentsByAvailability(agents, 'offline');
|
||||
const filteredAgents = [...onlineAgents, ...busyAgents, ...offlineAgents];
|
||||
return filteredAgents;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the availability status of the current user based on the current account
|
||||
* @param {Array} agents - List of agents
|
||||
* @param {Object} currentUser - Current user object
|
||||
* @param {number} currentAccountId - ID of the current account
|
||||
* @returns {Array} Updated list of agents with dynamic presence
|
||||
*/
|
||||
// Here we are updating the availability status of the current user dynamically
|
||||
// based on the current account availability status
|
||||
export const getAgentsByUpdatedPresence = (
|
||||
agents,
|
||||
currentUser,
|
||||
currentAccountId
|
||||
) => {
|
||||
const agentsWithDynamicPresenceUpdate = agents.map(item =>
|
||||
item.id === currentUser.id
|
||||
? {
|
||||
...item,
|
||||
availability_status: currentUser.accounts.find(
|
||||
account => account.id === currentAccountId
|
||||
).availability_status,
|
||||
}
|
||||
: item
|
||||
);
|
||||
return agentsWithDynamicPresenceUpdate;
|
||||
};
|
||||
|
||||
/**
|
||||
* Combines the filtered agents with the 'None' agent option if applicable.
|
||||
*
|
||||
* @param {Array} filteredAgentsByAvailability - The list of agents sorted by availability.
|
||||
* @param {boolean} includeNoneAgent - Whether to include the 'None' agent option.
|
||||
* @param {boolean} isAgentSelected - Whether an agent is currently selected.
|
||||
* @returns {Array} The combined list of agents, potentially including the 'None' agent.
|
||||
*/
|
||||
export const getCombinedAgents = (
|
||||
filteredAgentsByAvailability,
|
||||
includeNoneAgent,
|
||||
isAgentSelected
|
||||
) => {
|
||||
return [
|
||||
...(includeNoneAgent && isAgentSelected ? [createNoneAgent] : []),
|
||||
...filteredAgentsByAvailability,
|
||||
];
|
||||
};
|
||||
@@ -3,41 +3,16 @@ import {
|
||||
OPERATOR_TYPES_3,
|
||||
OPERATOR_TYPES_4,
|
||||
} from 'dashboard/routes/dashboard/settings/automation/operators';
|
||||
import {
|
||||
DEFAULT_MESSAGE_CREATED_CONDITION,
|
||||
DEFAULT_CONVERSATION_OPENED_CONDITION,
|
||||
DEFAULT_OTHER_CONDITION,
|
||||
DEFAULT_ACTIONS,
|
||||
MESSAGE_CONDITION_VALUES,
|
||||
PRIORITY_CONDITION_VALUES,
|
||||
} from 'dashboard/constants/automation';
|
||||
import filterQueryGenerator from './filterQueryGenerator';
|
||||
import actionQueryGenerator from './actionQueryGenerator';
|
||||
const MESSAGE_CONDITION_VALUES = [
|
||||
{
|
||||
id: 'incoming',
|
||||
name: 'Incoming Message',
|
||||
},
|
||||
{
|
||||
id: 'outgoing',
|
||||
name: 'Outgoing Message',
|
||||
},
|
||||
];
|
||||
|
||||
export const PRIORITY_CONDITION_VALUES = [
|
||||
{
|
||||
id: 'nil',
|
||||
name: 'None',
|
||||
},
|
||||
{
|
||||
id: 'low',
|
||||
name: 'Low',
|
||||
},
|
||||
{
|
||||
id: 'medium',
|
||||
name: 'Medium',
|
||||
},
|
||||
{
|
||||
id: 'high',
|
||||
name: 'High',
|
||||
},
|
||||
{
|
||||
id: 'urgent',
|
||||
name: 'Urgent',
|
||||
},
|
||||
];
|
||||
|
||||
export const getCustomAttributeInputType = key => {
|
||||
const customAttributeMap = {
|
||||
@@ -198,45 +173,16 @@ export const getFileName = (action, files = []) => {
|
||||
|
||||
export const getDefaultConditions = eventName => {
|
||||
if (eventName === 'message_created') {
|
||||
return [
|
||||
{
|
||||
attribute_key: 'message_type',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
},
|
||||
];
|
||||
return DEFAULT_MESSAGE_CREATED_CONDITION;
|
||||
}
|
||||
if (eventName === 'conversation_opened') {
|
||||
return [
|
||||
{
|
||||
attribute_key: 'browser_language',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
},
|
||||
];
|
||||
return DEFAULT_CONVERSATION_OPENED_CONDITION;
|
||||
}
|
||||
return [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
},
|
||||
];
|
||||
return DEFAULT_OTHER_CONDITION;
|
||||
};
|
||||
|
||||
export const getDefaultActions = () => {
|
||||
return [
|
||||
{
|
||||
action_name: 'assign_agent',
|
||||
action_params: [],
|
||||
},
|
||||
];
|
||||
return DEFAULT_ACTIONS;
|
||||
};
|
||||
|
||||
export const filterCustomAttributes = customAttributes => {
|
||||
@@ -297,3 +243,100 @@ export const generateCustomAttributes = (
|
||||
}
|
||||
return customAttributes;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get attributes for a given key from automation types.
|
||||
* @param {Object} automationTypes - Object containing automation types.
|
||||
* @param {string} key - The key to get attributes for.
|
||||
* @returns {Array} Array of condition objects for the given key.
|
||||
*/
|
||||
export const getAttributes = (automationTypes, key) => {
|
||||
return automationTypes[key].conditions;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the automation type for a given key.
|
||||
* @param {Object} automationTypes - Object containing automation types.
|
||||
* @param {Object} automation - The automation object.
|
||||
* @param {string} key - The key to get the automation type for.
|
||||
* @returns {Object} The automation type object.
|
||||
*/
|
||||
export const getAutomationType = (automationTypes, automation, key) => {
|
||||
return automationTypes[automation.event_name].conditions.find(
|
||||
condition => condition.key === key
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the input type for a given key.
|
||||
* @param {Array} allCustomAttributes - Array of all custom attributes.
|
||||
* @param {Object} automationTypes - Object containing automation types.
|
||||
* @param {Object} automation - The automation object.
|
||||
* @param {string} key - The key to get the input type for.
|
||||
* @returns {string} The input type.
|
||||
*/
|
||||
export const getInputType = (
|
||||
allCustomAttributes,
|
||||
automationTypes,
|
||||
automation,
|
||||
key
|
||||
) => {
|
||||
const customAttribute = isACustomAttribute(allCustomAttributes, key);
|
||||
if (customAttribute) {
|
||||
return getCustomAttributeInputType(customAttribute.attribute_display_type);
|
||||
}
|
||||
const type = getAutomationType(automationTypes, automation, key);
|
||||
return type.inputType;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get operators for a given key.
|
||||
* @param {Array} allCustomAttributes - Array of all custom attributes.
|
||||
* @param {Object} automationTypes - Object containing automation types.
|
||||
* @param {Object} automation - The automation object.
|
||||
* @param {string} mode - The mode ('edit' or other).
|
||||
* @param {string} key - The key to get operators for.
|
||||
* @returns {Array} Array of operators.
|
||||
*/
|
||||
export const getOperators = (
|
||||
allCustomAttributes,
|
||||
automationTypes,
|
||||
automation,
|
||||
mode,
|
||||
key
|
||||
) => {
|
||||
if (mode === 'edit') {
|
||||
const customAttribute = isACustomAttribute(allCustomAttributes, key);
|
||||
if (customAttribute) {
|
||||
return getOperatorTypes(customAttribute.attribute_display_type);
|
||||
}
|
||||
}
|
||||
const type = getAutomationType(automationTypes, automation, key);
|
||||
return type.filterOperators;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the custom attribute type for a given key.
|
||||
* @param {Object} automationTypes - Object containing automation types.
|
||||
* @param {Object} automation - The automation object.
|
||||
* @param {string} key - The key to get the custom attribute type for.
|
||||
* @returns {string} The custom attribute type.
|
||||
*/
|
||||
export const getCustomAttributeType = (automationTypes, automation, key) => {
|
||||
return automationTypes[automation.event_name].conditions.find(
|
||||
i => i.key === key
|
||||
).customAttributeType;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine if an action input should be shown.
|
||||
* @param {Array} automationActionTypes - Array of automation action type objects.
|
||||
* @param {string} action - The action to check.
|
||||
* @returns {boolean} True if the action input should be shown, false otherwise.
|
||||
*/
|
||||
export const showActionInput = (automationActionTypes, action) => {
|
||||
if (action === 'send_email_to_team' || action === 'send_message')
|
||||
return false;
|
||||
const type = automationActionTypes.find(i => i.key === action).inputType;
|
||||
return !!type;
|
||||
};
|
||||
|
||||
+3
-2
@@ -8,7 +8,7 @@ import {
|
||||
CMD_SEND_TRANSCRIPT,
|
||||
CMD_SNOOZE_CONVERSATION,
|
||||
CMD_UNMUTE_CONVERSATION,
|
||||
} from './commandBarBusEvents';
|
||||
} from 'dashboard/helper/commandbar/events';
|
||||
|
||||
import {
|
||||
ICON_MUTE_CONVERSATION,
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
ICON_SEND_TRANSCRIPT,
|
||||
ICON_SNOOZE_CONVERSATION,
|
||||
ICON_UNMUTE_CONVERSATION,
|
||||
} from './CommandBarIcons';
|
||||
} from 'dashboard/helper/commandbar/icons';
|
||||
|
||||
const SNOOZE_OPTIONS = wootConstants.SNOOZE_OPTIONS;
|
||||
|
||||
@@ -46,6 +46,7 @@ export const SNOOZE_CONVERSATION_ACTIONS = [
|
||||
{
|
||||
id: 'snooze_conversation',
|
||||
title: 'COMMAND_BAR.COMMANDS.SNOOZE_CONVERSATION',
|
||||
section: 'COMMAND_BAR.SECTIONS.CONVERSATION',
|
||||
icon: ICON_SNOOZE_CONVERSATION,
|
||||
children: Object.values(SNOOZE_OPTIONS),
|
||||
},
|
||||
@@ -11,6 +11,7 @@ const FEATURE_HELP_URLS = {
|
||||
help_center: 'https://chwt.app/hc/help-center',
|
||||
integrations: 'https://chwt.app/hc/integrations',
|
||||
labels: 'https://chwt.app/hc/labels',
|
||||
macros: 'https://chwt.app/hc/macros',
|
||||
message_reply_to: 'https://chwt.app/hc/reply-to',
|
||||
reports: 'https://chwt.app/hc/reports',
|
||||
sla: 'https://chwt.app/hc/sla',
|
||||
|
||||
@@ -7,6 +7,15 @@ export const hasPermissions = (
|
||||
);
|
||||
};
|
||||
|
||||
export const getCurrentAccount = ({ accounts } = {}, accountId = null) => {
|
||||
return accounts.find(account => Number(account.id) === Number(accountId));
|
||||
};
|
||||
|
||||
export const getUserPermissions = (user, accountId) => {
|
||||
const currentAccount = getCurrentAccount(user, accountId) || {};
|
||||
return currentAccount.permissions || [];
|
||||
};
|
||||
|
||||
const isPermissionsPresentInRoute = route =>
|
||||
route.meta && route.meta.permissions;
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { hasPermissions } from './permissionsHelper';
|
||||
|
||||
// eslint-disable-next-line default-param-last
|
||||
export const getCurrentAccount = ({ accounts } = {}, accountId) => {
|
||||
return accounts.find(account => account.id === accountId);
|
||||
};
|
||||
import {
|
||||
hasPermissions,
|
||||
getUserPermissions,
|
||||
getCurrentAccount,
|
||||
} from './permissionsHelper';
|
||||
|
||||
export const routeIsAccessibleFor = (route, userPermissions = []) => {
|
||||
const { meta: { permissions: routePermissions = [] } = {} } = route;
|
||||
@@ -19,7 +18,9 @@ const validateActiveAccountRoutes = (to, user) => {
|
||||
return accountDashboardURL;
|
||||
}
|
||||
|
||||
const isAccessible = routeIsAccessibleFor(to, user.permissions);
|
||||
const userPermissions = getUserPermissions(user, to.params.accountId);
|
||||
|
||||
const isAccessible = routeIsAccessibleFor(to, userPermissions);
|
||||
// If the route is not accessible for the user, return to dashboard screen
|
||||
return isAccessible ? null : accountDashboardURL;
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ const initializeAudioAlerts = user => {
|
||||
enable_audio_alerts: audioAlertType,
|
||||
alert_if_unread_assigned_conversation_exist: alertIfUnreadConversationExist,
|
||||
notification_tone: audioAlertTone,
|
||||
// UI Settings can be undefined initally as we don't send the
|
||||
// UI Settings can be undefined initially as we don't send the
|
||||
// entire payload for the user during the signup process.
|
||||
} = uiSettings || {};
|
||||
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import {
|
||||
getAgentsByAvailability,
|
||||
getSortedAgentsByAvailability,
|
||||
getAgentsByUpdatedPresence,
|
||||
getCombinedAgents,
|
||||
createNoneAgent,
|
||||
} from '../agentHelper';
|
||||
import {
|
||||
allAgentsData,
|
||||
onlineAgentsData,
|
||||
busyAgentsData,
|
||||
offlineAgentsData,
|
||||
sortedByAvailability,
|
||||
formattedAgentsByPresenceOnline,
|
||||
formattedAgentsByPresenceOffline,
|
||||
} from 'dashboard/helper/specs/fixtures/agentFixtures';
|
||||
|
||||
describe('agentHelper', () => {
|
||||
describe('getAgentsByAvailability', () => {
|
||||
it('returns agents by availability', () => {
|
||||
expect(getAgentsByAvailability(allAgentsData, 'online')).toEqual(
|
||||
onlineAgentsData
|
||||
);
|
||||
expect(getAgentsByAvailability(allAgentsData, 'busy')).toEqual(
|
||||
busyAgentsData
|
||||
);
|
||||
expect(getAgentsByAvailability(allAgentsData, 'offline')).toEqual(
|
||||
offlineAgentsData
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSortedAgentsByAvailability', () => {
|
||||
it('returns sorted agents by availability', () => {
|
||||
expect(getSortedAgentsByAvailability(allAgentsData)).toEqual(
|
||||
sortedByAvailability
|
||||
);
|
||||
});
|
||||
|
||||
it('returns an empty array when given an empty input', () => {
|
||||
expect(getSortedAgentsByAvailability([])).toEqual([]);
|
||||
});
|
||||
|
||||
it('maintains the order of agents with the same availability status', () => {
|
||||
const result = getSortedAgentsByAvailability(allAgentsData);
|
||||
expect(result[2].name).toBe('Honey Bee');
|
||||
expect(result[3].name).toBe('Samuel Keta');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAgentsByUpdatedPresence', () => {
|
||||
it('returns agents with updated presence', () => {
|
||||
const currentUser = {
|
||||
id: 1,
|
||||
accounts: [{ id: 1, availability_status: 'offline' }],
|
||||
};
|
||||
const currentAccountId = 1;
|
||||
|
||||
expect(
|
||||
getAgentsByUpdatedPresence(
|
||||
formattedAgentsByPresenceOnline,
|
||||
currentUser,
|
||||
currentAccountId
|
||||
)
|
||||
).toEqual(formattedAgentsByPresenceOffline);
|
||||
});
|
||||
|
||||
it('does not modify other agents presence', () => {
|
||||
const currentUser = {
|
||||
id: 2,
|
||||
accounts: [{ id: 1, availability_status: 'offline' }],
|
||||
};
|
||||
const currentAccountId = 1;
|
||||
|
||||
expect(
|
||||
getAgentsByUpdatedPresence(
|
||||
formattedAgentsByPresenceOnline,
|
||||
currentUser,
|
||||
currentAccountId
|
||||
)
|
||||
).toEqual(formattedAgentsByPresenceOnline);
|
||||
});
|
||||
|
||||
it('handles empty agent list', () => {
|
||||
const currentUser = {
|
||||
id: 1,
|
||||
accounts: [{ id: 1, availability_status: 'offline' }],
|
||||
};
|
||||
const currentAccountId = 1;
|
||||
|
||||
expect(
|
||||
getAgentsByUpdatedPresence([], currentUser, currentAccountId)
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCombinedAgents', () => {
|
||||
it('includes None agent when includeNoneAgent is true and isAgentSelected is true', () => {
|
||||
const result = getCombinedAgents(sortedByAvailability, true, true);
|
||||
expect(result).toEqual([createNoneAgent, ...sortedByAvailability]);
|
||||
expect(result.length).toBe(sortedByAvailability.length + 1);
|
||||
expect(result[0]).toEqual(createNoneAgent);
|
||||
});
|
||||
|
||||
it('excludes None agent when includeNoneAgent is false', () => {
|
||||
const result = getCombinedAgents(sortedByAvailability, false, true);
|
||||
expect(result).toEqual(sortedByAvailability);
|
||||
expect(result.length).toBe(sortedByAvailability.length);
|
||||
expect(result[0]).not.toEqual(createNoneAgent);
|
||||
});
|
||||
|
||||
it('excludes None agent when isAgentSelected is false', () => {
|
||||
const result = getCombinedAgents(sortedByAvailability, true, false);
|
||||
expect(result).toEqual(sortedByAvailability);
|
||||
expect(result.length).toBe(sortedByAvailability.length);
|
||||
expect(result[0]).not.toEqual(createNoneAgent);
|
||||
});
|
||||
|
||||
it('returns only filtered agents when both includeNoneAgent and isAgentSelected are false', () => {
|
||||
const result = getCombinedAgents(sortedByAvailability, false, false);
|
||||
expect(result).toEqual(sortedByAvailability);
|
||||
expect(result.length).toBe(sortedByAvailability.length);
|
||||
});
|
||||
|
||||
it('handles empty filteredAgentsByAvailability array', () => {
|
||||
const result = getCombinedAgents([], true, true);
|
||||
expect(result).toEqual([createNoneAgent]);
|
||||
expect(result.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
+194
-102
@@ -11,11 +11,11 @@ import {
|
||||
contactAttrs,
|
||||
conversationAttrs,
|
||||
expectedOutputForCustomAttributeGenerator,
|
||||
} from './automationFixtures';
|
||||
} from './fixtures/automationFixtures';
|
||||
import { AUTOMATIONS } from 'dashboard/routes/dashboard/settings/automation/constants';
|
||||
|
||||
describe('automationMethodsMixin', () => {
|
||||
it('getCustomAttributeInputType returns the attribute input type', () => {
|
||||
describe('getCustomAttributeInputType', () => {
|
||||
it('returns the attribute input type', () => {
|
||||
expect(helpers.getCustomAttributeInputType('date')).toEqual('date');
|
||||
expect(helpers.getCustomAttributeInputType('date')).not.toEqual(
|
||||
'some_random_value'
|
||||
@@ -31,33 +31,32 @@ describe('automationMethodsMixin', () => {
|
||||
'plain_text'
|
||||
);
|
||||
});
|
||||
it('isACustomAttribute returns the custom attribute value if true', () => {
|
||||
});
|
||||
|
||||
describe('isACustomAttribute', () => {
|
||||
it('returns the custom attribute value if true', () => {
|
||||
expect(
|
||||
helpers.isACustomAttribute(customAttributes, 'signed_up_at')
|
||||
).toBeTruthy();
|
||||
expect(helpers.isACustomAttribute(customAttributes, 'status')).toBeFalsy();
|
||||
});
|
||||
it('getCustomAttributeListDropdownValues returns the attribute dropdown values', () => {
|
||||
});
|
||||
|
||||
describe('getCustomAttributeListDropdownValues', () => {
|
||||
it('returns the attribute dropdown values', () => {
|
||||
const myListValues = [
|
||||
{
|
||||
id: 'item1',
|
||||
name: 'item1',
|
||||
},
|
||||
{
|
||||
id: 'item2',
|
||||
name: 'item2',
|
||||
},
|
||||
{
|
||||
id: 'item3',
|
||||
name: 'item3',
|
||||
},
|
||||
{ id: 'item1', name: 'item1' },
|
||||
{ id: 'item2', name: 'item2' },
|
||||
{ id: 'item3', name: 'item3' },
|
||||
];
|
||||
expect(
|
||||
helpers.getCustomAttributeListDropdownValues(customAttributes, 'my_list')
|
||||
).toEqual(myListValues);
|
||||
});
|
||||
});
|
||||
|
||||
it('isCustomAttributeCheckbox checks if attribute is a checkbox', () => {
|
||||
describe('isCustomAttributeCheckbox', () => {
|
||||
it('checks if attribute is a checkbox', () => {
|
||||
expect(
|
||||
helpers.isCustomAttributeCheckbox(customAttributes, 'prime_user')
|
||||
.attribute_display_type
|
||||
@@ -70,13 +69,19 @@ describe('automationMethodsMixin', () => {
|
||||
helpers.isCustomAttributeCheckbox(customAttributes, 'my_list')
|
||||
).not.toEqual('checkbox');
|
||||
});
|
||||
it('isCustomAttributeList checks if attribute is a list', () => {
|
||||
});
|
||||
|
||||
describe('isCustomAttributeList', () => {
|
||||
it('checks if attribute is a list', () => {
|
||||
expect(
|
||||
helpers.isCustomAttributeList(customAttributes, 'my_list')
|
||||
.attribute_display_type
|
||||
).toEqual('list');
|
||||
});
|
||||
it('getOperatorTypes returns the correct custom attribute operators', () => {
|
||||
});
|
||||
|
||||
describe('getOperatorTypes', () => {
|
||||
it('returns the correct custom attribute operators', () => {
|
||||
expect(helpers.getOperatorTypes('list')).toEqual(OPERATOR_TYPES_1);
|
||||
expect(helpers.getOperatorTypes('text')).toEqual(OPERATOR_TYPES_3);
|
||||
expect(helpers.getOperatorTypes('number')).toEqual(OPERATOR_TYPES_1);
|
||||
@@ -85,93 +90,44 @@ describe('automationMethodsMixin', () => {
|
||||
expect(helpers.getOperatorTypes('checkbox')).toEqual(OPERATOR_TYPES_1);
|
||||
expect(helpers.getOperatorTypes('some_random')).toEqual(OPERATOR_TYPES_1);
|
||||
});
|
||||
it('generateConditionOptions returns expected conditions options array', () => {
|
||||
const testConditions = [
|
||||
{
|
||||
id: 123,
|
||||
title: 'Fayaz',
|
||||
email: 'test@test.com',
|
||||
},
|
||||
{
|
||||
title: 'John',
|
||||
id: 324,
|
||||
email: 'test@john.com',
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
describe('generateConditionOptions', () => {
|
||||
it('returns expected conditions options array', () => {
|
||||
const testConditions = [
|
||||
{ id: 123, title: 'Fayaz', email: 'test@test.com' },
|
||||
{ title: 'John', id: 324, email: 'test@john.com' },
|
||||
];
|
||||
const expectedConditions = [
|
||||
{
|
||||
id: 123,
|
||||
name: 'Fayaz',
|
||||
},
|
||||
{
|
||||
id: 324,
|
||||
name: 'John',
|
||||
},
|
||||
{ id: 123, name: 'Fayaz' },
|
||||
{ id: 324, name: 'John' },
|
||||
];
|
||||
expect(helpers.generateConditionOptions(testConditions)).toEqual(
|
||||
expectedConditions
|
||||
);
|
||||
});
|
||||
it('getActionOptions returns expected actions options array', () => {
|
||||
});
|
||||
|
||||
describe('getActionOptions', () => {
|
||||
it('returns expected actions options array', () => {
|
||||
const expectedOptions = [
|
||||
{
|
||||
id: 'testlabel',
|
||||
name: 'testlabel',
|
||||
},
|
||||
{
|
||||
id: 'snoozes',
|
||||
name: 'snoozes',
|
||||
},
|
||||
{ id: 'testlabel', name: 'testlabel' },
|
||||
{ id: 'snoozes', name: 'snoozes' },
|
||||
];
|
||||
expect(helpers.getActionOptions({ labels, type: 'add_label' })).toEqual(
|
||||
expectedOptions
|
||||
);
|
||||
});
|
||||
it('getConditionOptions returns expected conditions options', () => {
|
||||
});
|
||||
|
||||
describe('getConditionOptions', () => {
|
||||
it('returns expected conditions options', () => {
|
||||
const testOptions = [
|
||||
{
|
||||
id: 'open',
|
||||
name: 'Open',
|
||||
},
|
||||
{
|
||||
id: 'resolved',
|
||||
name: 'Resolved',
|
||||
},
|
||||
{
|
||||
id: 'pending',
|
||||
name: 'Pending',
|
||||
},
|
||||
{
|
||||
id: 'snoozed',
|
||||
name: 'Snoozed',
|
||||
},
|
||||
{
|
||||
id: 'all',
|
||||
name: 'All',
|
||||
},
|
||||
];
|
||||
const expectedOptions = [
|
||||
{
|
||||
id: 'open',
|
||||
name: 'Open',
|
||||
},
|
||||
{
|
||||
id: 'resolved',
|
||||
name: 'Resolved',
|
||||
},
|
||||
{
|
||||
id: 'pending',
|
||||
name: 'Pending',
|
||||
},
|
||||
{
|
||||
id: 'snoozed',
|
||||
name: 'Snoozed',
|
||||
},
|
||||
{
|
||||
id: 'all',
|
||||
name: 'All',
|
||||
},
|
||||
{ id: 'open', name: 'Open' },
|
||||
{ id: 'resolved', name: 'Resolved' },
|
||||
{ id: 'pending', name: 'Pending' },
|
||||
{ id: 'snoozed', name: 'Snoozed' },
|
||||
{ id: 'all', name: 'All' },
|
||||
];
|
||||
expect(
|
||||
helpers.getConditionOptions({
|
||||
@@ -180,14 +136,20 @@ describe('automationMethodsMixin', () => {
|
||||
statusFilterOptions: testOptions,
|
||||
type: 'status',
|
||||
})
|
||||
).toEqual(expectedOptions);
|
||||
).toEqual(testOptions);
|
||||
});
|
||||
it('getFileName returns the correct file name', () => {
|
||||
});
|
||||
|
||||
describe('getFileName', () => {
|
||||
it('returns the correct file name', () => {
|
||||
expect(
|
||||
helpers.getFileName(automation.actions[0], automation.files)
|
||||
).toEqual('pfp.jpeg');
|
||||
});
|
||||
it('getDefaultConditions returns the resp default condition model', () => {
|
||||
});
|
||||
|
||||
describe('getDefaultConditions', () => {
|
||||
it('returns the resp default condition model', () => {
|
||||
const messageCreatedModel = [
|
||||
{
|
||||
attribute_key: 'message_type',
|
||||
@@ -211,7 +173,10 @@ describe('automationMethodsMixin', () => {
|
||||
);
|
||||
expect(helpers.getDefaultConditions()).toEqual(genericConditionModel);
|
||||
});
|
||||
it('getDefaultActions returns the resp default action model', () => {
|
||||
});
|
||||
|
||||
describe('getDefaultActions', () => {
|
||||
it('returns the resp default action model', () => {
|
||||
const genericActionModel = [
|
||||
{
|
||||
action_name: 'assign_agent',
|
||||
@@ -220,7 +185,10 @@ describe('automationMethodsMixin', () => {
|
||||
];
|
||||
expect(helpers.getDefaultActions()).toEqual(genericActionModel);
|
||||
});
|
||||
it('filterCustomAttributes filters the raw custom attributes', () => {
|
||||
});
|
||||
|
||||
describe('filterCustomAttributes', () => {
|
||||
it('filters the raw custom attributes', () => {
|
||||
const filteredAttributes = [
|
||||
{ key: 'signed_up_at', name: 'Signed Up At', type: 'date' },
|
||||
{ key: 'prime_user', name: 'Prime User', type: 'checkbox' },
|
||||
@@ -235,7 +203,10 @@ describe('automationMethodsMixin', () => {
|
||||
filteredAttributes
|
||||
);
|
||||
});
|
||||
it('getStandardAttributeInputType returns the resp default action model', () => {
|
||||
});
|
||||
|
||||
describe('getStandardAttributeInputType', () => {
|
||||
it('returns the resp default action model', () => {
|
||||
expect(
|
||||
helpers.getStandardAttributeInputType(
|
||||
AUTOMATIONS,
|
||||
@@ -258,7 +229,10 @@ describe('automationMethodsMixin', () => {
|
||||
)
|
||||
).toEqual('plain_text');
|
||||
});
|
||||
it('generateAutomationPayload returns the resp default action model', () => {
|
||||
});
|
||||
|
||||
describe('generateAutomationPayload', () => {
|
||||
it('returns the resp default action model', () => {
|
||||
const testPayload = {
|
||||
name: 'Test',
|
||||
description: 'This is a test',
|
||||
@@ -300,7 +274,10 @@ describe('automationMethodsMixin', () => {
|
||||
expectedPayload
|
||||
);
|
||||
});
|
||||
it('isCustomAttribute returns the resp default action model', () => {
|
||||
});
|
||||
|
||||
describe('isCustomAttribute', () => {
|
||||
it('returns the resp default action model', () => {
|
||||
const attrs = helpers.filterCustomAttributes(customAttributes);
|
||||
expect(helpers.isCustomAttribute(attrs, 'my_list')).toBeTruthy();
|
||||
expect(helpers.isCustomAttribute(attrs, 'my_check')).toBeTruthy();
|
||||
@@ -309,8 +286,10 @@ describe('automationMethodsMixin', () => {
|
||||
expect(helpers.isCustomAttribute(attrs, 'prime_user')).toBeTruthy();
|
||||
expect(helpers.isCustomAttribute(attrs, 'hello')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it('generateCustomAttributes generates and returns correct condition attribute', () => {
|
||||
describe('generateCustomAttributes', () => {
|
||||
it('generates and returns correct condition attribute', () => {
|
||||
expect(
|
||||
helpers.generateCustomAttributes(
|
||||
conversationAttrs,
|
||||
@@ -321,3 +300,116 @@ describe('automationMethodsMixin', () => {
|
||||
).toEqual(expectedOutputForCustomAttributeGenerator);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAttributes', () => {
|
||||
it('returns the conditions for the given automation type', () => {
|
||||
const result = helpers.getAttributes(AUTOMATIONS, 'message_created');
|
||||
expect(result).toEqual(AUTOMATIONS.message_created.conditions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAttributes', () => {
|
||||
it('returns the conditions for the given automation type', () => {
|
||||
const result = helpers.getAttributes(AUTOMATIONS, 'message_created');
|
||||
expect(result).toEqual(AUTOMATIONS.message_created.conditions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAutomationType', () => {
|
||||
it('returns the automation type for the given key', () => {
|
||||
const mockAutomation = { event_name: 'message_created' };
|
||||
const result = helpers.getAutomationType(
|
||||
AUTOMATIONS,
|
||||
mockAutomation,
|
||||
'message_type'
|
||||
);
|
||||
expect(result).toEqual(
|
||||
AUTOMATIONS.message_created.conditions.find(c => c.key === 'message_type')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInputType', () => {
|
||||
it('returns the input type for a custom attribute', () => {
|
||||
const mockAutomation = { event_name: 'message_created' };
|
||||
const result = helpers.getInputType(
|
||||
customAttributes,
|
||||
AUTOMATIONS,
|
||||
mockAutomation,
|
||||
'signed_up_at'
|
||||
);
|
||||
expect(result).toEqual('date');
|
||||
});
|
||||
|
||||
it('returns the input type for a standard attribute', () => {
|
||||
const mockAutomation = { event_name: 'message_created' };
|
||||
const result = helpers.getInputType(
|
||||
customAttributes,
|
||||
AUTOMATIONS,
|
||||
mockAutomation,
|
||||
'message_type'
|
||||
);
|
||||
expect(result).toEqual('search_select');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOperators', () => {
|
||||
it('returns operators for a custom attribute in edit mode', () => {
|
||||
const mockAutomation = { event_name: 'message_created' };
|
||||
const result = helpers.getOperators(
|
||||
customAttributes,
|
||||
AUTOMATIONS,
|
||||
mockAutomation,
|
||||
'edit',
|
||||
'signed_up_at'
|
||||
);
|
||||
expect(result).toEqual(OPERATOR_TYPES_4);
|
||||
});
|
||||
|
||||
it('returns operators for a standard attribute', () => {
|
||||
const mockAutomation = { event_name: 'message_created' };
|
||||
const result = helpers.getOperators(
|
||||
customAttributes,
|
||||
AUTOMATIONS,
|
||||
mockAutomation,
|
||||
'create',
|
||||
'message_type'
|
||||
);
|
||||
expect(result).toEqual(
|
||||
AUTOMATIONS.message_created.conditions.find(c => c.key === 'message_type')
|
||||
.filterOperators
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCustomAttributeType', () => {
|
||||
it('returns the custom attribute type for the given key', () => {
|
||||
const mockAutomation = { event_name: 'message_created' };
|
||||
const result = helpers.getCustomAttributeType(
|
||||
AUTOMATIONS,
|
||||
mockAutomation,
|
||||
'message_type'
|
||||
);
|
||||
expect(result).toEqual(
|
||||
AUTOMATIONS.message_created.conditions.find(c => c.key === 'message_type')
|
||||
.customAttributeType
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('showActionInput', () => {
|
||||
it('returns false for send_email_to_team and send_message actions', () => {
|
||||
expect(helpers.showActionInput([], 'send_email_to_team')).toBe(false);
|
||||
expect(helpers.showActionInput([], 'send_message')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true if the action has an input type', () => {
|
||||
const mockActionTypes = [{ key: 'add_label', inputType: 'select' }];
|
||||
expect(helpers.showActionInput(mockActionTypes, 'add_label')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false if the action does not have an input type', () => {
|
||||
const mockActionTypes = [{ key: 'some_action', inputType: null }];
|
||||
expect(helpers.showActionInput(mockActionTypes, 'some_action')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,184 @@
|
||||
export const allAgentsData = [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'John K',
|
||||
confirmed: true,
|
||||
email: 'john@chatwoot.com',
|
||||
id: 1,
|
||||
name: 'John Kennady',
|
||||
role: 'administrator',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Samuel K',
|
||||
confirmed: true,
|
||||
email: 'samuel@chatwoot.com',
|
||||
id: 2,
|
||||
name: 'Samuel Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
available_name: 'James K',
|
||||
confirmed: true,
|
||||
email: 'james@chatwoot.com',
|
||||
id: 3,
|
||||
name: 'James Koti',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Honey',
|
||||
confirmed: true,
|
||||
email: 'bee@chatwoot.com',
|
||||
id: 4,
|
||||
name: 'Honey Bee',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'Abraham',
|
||||
confirmed: true,
|
||||
email: 'abraham@chatwoot.com',
|
||||
id: 5,
|
||||
name: 'Abraham Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
];
|
||||
export const onlineAgentsData = [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'Abraham',
|
||||
confirmed: true,
|
||||
email: 'abraham@chatwoot.com',
|
||||
id: 5,
|
||||
name: 'Abraham Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'John K',
|
||||
confirmed: true,
|
||||
email: 'john@chatwoot.com',
|
||||
id: 1,
|
||||
name: 'John Kennady',
|
||||
role: 'administrator',
|
||||
},
|
||||
];
|
||||
export const busyAgentsData = [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Honey',
|
||||
confirmed: true,
|
||||
email: 'bee@chatwoot.com',
|
||||
id: 4,
|
||||
name: 'Honey Bee',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Samuel K',
|
||||
confirmed: true,
|
||||
email: 'samuel@chatwoot.com',
|
||||
id: 2,
|
||||
name: 'Samuel Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
];
|
||||
export const offlineAgentsData = [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
available_name: 'James K',
|
||||
confirmed: true,
|
||||
email: 'james@chatwoot.com',
|
||||
id: 3,
|
||||
name: 'James Koti',
|
||||
role: 'agent',
|
||||
},
|
||||
];
|
||||
export const sortedByAvailability = [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'Abraham',
|
||||
confirmed: true,
|
||||
email: 'abraham@chatwoot.com',
|
||||
id: 5,
|
||||
name: 'Abraham Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'John K',
|
||||
confirmed: true,
|
||||
email: 'john@chatwoot.com',
|
||||
id: 1,
|
||||
name: 'John Kennady',
|
||||
role: 'administrator',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Honey',
|
||||
confirmed: true,
|
||||
email: 'bee@chatwoot.com',
|
||||
id: 4,
|
||||
name: 'Honey Bee',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Samuel K',
|
||||
confirmed: true,
|
||||
email: 'samuel@chatwoot.com',
|
||||
id: 2,
|
||||
name: 'Samuel Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
available_name: 'James K',
|
||||
confirmed: true,
|
||||
email: 'james@chatwoot.com',
|
||||
id: 3,
|
||||
name: 'James Koti',
|
||||
role: 'agent',
|
||||
},
|
||||
];
|
||||
export const formattedAgentsByPresenceOnline = [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'Abraham',
|
||||
confirmed: true,
|
||||
email: 'abr@chatwoot.com',
|
||||
id: 1,
|
||||
name: 'Abraham Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
];
|
||||
export const formattedAgentsByPresenceOffline = [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
available_name: 'Abraham',
|
||||
confirmed: true,
|
||||
email: 'abr@chatwoot.com',
|
||||
id: 1,
|
||||
name: 'Abraham Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
];
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import allLanguages from '../../../dashboard/components/widgets/conversation/advancedFilterItems/languages.js';
|
||||
import allLanguages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||
|
||||
import allCountries from '../../../shared/constants/countries.js';
|
||||
import allCountries from 'shared/constants/countries.js';
|
||||
|
||||
export const customAttributes = [
|
||||
{
|
||||
@@ -1,8 +1,31 @@
|
||||
import {
|
||||
buildPermissionsFromRouter,
|
||||
getCurrentAccount,
|
||||
getUserPermissions,
|
||||
hasPermissions,
|
||||
} from '../permissionsHelper';
|
||||
|
||||
describe('#getCurrentAccount', () => {
|
||||
it('should return the current account', () => {
|
||||
expect(getCurrentAccount({ accounts: [{ id: 1 }] }, 1)).toEqual({ id: 1 });
|
||||
expect(getCurrentAccount({ accounts: [] }, 1)).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getUserPermissions', () => {
|
||||
it('should return the correct permissions', () => {
|
||||
const user = {
|
||||
accounts: [
|
||||
{ id: 1, permissions: ['conversations_manage'] },
|
||||
{ id: 3, permissions: ['contacts_manage'] },
|
||||
],
|
||||
};
|
||||
expect(getUserPermissions(user, 1)).toEqual(['conversations_manage']);
|
||||
expect(getUserPermissions(user, '3')).toEqual(['contacts_manage']);
|
||||
expect(getUserPermissions(user, 2)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasPermissions', () => {
|
||||
it('returns true if permission is present', () => {
|
||||
expect(
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
import {
|
||||
getConversationDashboardRoute,
|
||||
getCurrentAccount,
|
||||
isAConversationRoute,
|
||||
routeIsAccessibleFor,
|
||||
validateLoggedInRoutes,
|
||||
isAInboxViewRoute,
|
||||
} from '../routeHelpers';
|
||||
|
||||
describe('#getCurrentAccount', () => {
|
||||
it('should return the current account', () => {
|
||||
expect(getCurrentAccount({ accounts: [{ id: 1 }] }, 1)).toEqual({ id: 1 });
|
||||
expect(getCurrentAccount({ accounts: [] }, 1)).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#routeIsAccessibleFor', () => {
|
||||
it('should return the correct access', () => {
|
||||
let route = { meta: { permissions: ['administrator'] } };
|
||||
|
||||
@@ -241,25 +241,29 @@
|
||||
"ONBOARDING": {
|
||||
"TITLE": "Hey 👋, Welcome to %{installationName}!",
|
||||
"DESCRIPTION": "Thanks for signing up. We want you to get the most out of %{installationName}. Here are a few things you can do in %{installationName} to make the experience delightful.",
|
||||
"GREETING_MORNING": "👋 Good morning, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_AFTERNOON": "👋 Good afternoon, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_EVENING": "👋 Good evening, %{name}. Welcome to %{installationName}.",
|
||||
"READ_LATEST_UPDATES": "Read our latest updates",
|
||||
"ALL_CONVERSATION": {
|
||||
"TITLE": "All your conversations in one place",
|
||||
"DESCRIPTION": "View all the conversations from your customers in one single dashboard. You can filter the conversations by the incoming channel, label and status."
|
||||
"DESCRIPTION": "View all the conversations from your customers in one single dashboard. You can filter the conversations by the incoming channel, label and status.",
|
||||
"NEW_LINK": "Click here to create an inbox"
|
||||
},
|
||||
"TEAM_MEMBERS": {
|
||||
"TITLE": "Invite your team members",
|
||||
"DESCRIPTION": "Since you are getting ready to talk to your customer, bring in your teammates to assist you. You can invite your teammates by adding their email addresses to the agent list.",
|
||||
"NEW_LINK": "Click here to invite a team member"
|
||||
},
|
||||
"INBOXES": {
|
||||
"TITLE": "Connect Inboxes",
|
||||
"DESCRIPTION": "Connect various channels through which your customers would be talking to you. It can be a website live-chat, your Facebook page or even your WhatsApp number.",
|
||||
"NEW_LINK": "Click here to create an inbox"
|
||||
},
|
||||
"LABELS": {
|
||||
"TITLE": "Organize conversations with labels",
|
||||
"DESCRIPTION": "Labels provide an easier way to categorize your conversation. Create some labels like #support-enquiry, #billing-question etc., so that you can use them in a conversation later.",
|
||||
"NEW_LINK": "Click here to create tags"
|
||||
},
|
||||
"CANNED_RESPONSES": {
|
||||
"TITLE": "Create canned responses",
|
||||
"DESCRIPTION": "Pre-written quick reply templates help you quickly respond to a conversation. Agents can type the '/' character followed by the shortcode to insert a response.",
|
||||
"NEW_LINK": "Click here to create a canned response"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_SIDEBAR": {
|
||||
|
||||
@@ -241,25 +241,29 @@
|
||||
"ONBOARDING": {
|
||||
"TITLE": "اهلاً 👋، مرحباً بك في %{installationName}!",
|
||||
"DESCRIPTION": "شكرا للتسجيل. نريدك أن تحصل على أقصى استفادة من %{installationName}. إليك بعض الأشياء التي يمكنك القيام بها في %{installationName} لجعل التجربة رائعة.",
|
||||
"GREETING_MORNING": "👋 Good morning, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_AFTERNOON": "👋 Good afternoon, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_EVENING": "👋 Good evening, %{name}. Welcome to %{installationName}.",
|
||||
"READ_LATEST_UPDATES": "اطلع على آخر التحديثات",
|
||||
"ALL_CONVERSATION": {
|
||||
"TITLE": "جميع محادثاتك في مكان واحد",
|
||||
"DESCRIPTION": "عرض جميع المحادثات من عملائك في لوحة تحكم واحدة. يمكنك تصفية المحادثات حسب القناة الواردة والتسمية والحالة."
|
||||
"DESCRIPTION": "عرض جميع المحادثات من عملائك في لوحة تحكم واحدة. يمكنك تصفية المحادثات حسب القناة الواردة والتسمية والحالة.",
|
||||
"NEW_LINK": "انقر هنا لإنشاء صندوق وارد"
|
||||
},
|
||||
"TEAM_MEMBERS": {
|
||||
"TITLE": "قم بدعوة أعضاء فريقك",
|
||||
"DESCRIPTION": "عندما تصبح جاهز للتخاطب مع عميلك، احضر زملائك في الفريق لمساعدتك. يمكنك دعوة زملائك في الفريق بإضافة عنوان البريد الإلكتروني الخاص بهم إلى قائمة الوكلاء.",
|
||||
"NEW_LINK": "انقر هنا لدعوة عضو في الفريق"
|
||||
},
|
||||
"INBOXES": {
|
||||
"TITLE": "ربط الصناديق الواردة",
|
||||
"DESCRIPTION": "قم بتوصيل مختلف القنوات التي من خلالها يتحدث عملاؤك إليك. يمكن أن تكون محادثة مباشرة على موقع الويب أو صفحة فيسبوك أو تويتر أو حتى رقم WhatsApp الخاص بك.",
|
||||
"NEW_LINK": "انقر هنا لإنشاء صندوق وارد"
|
||||
},
|
||||
"LABELS": {
|
||||
"TITLE": "تنظيم المحادثات مع التسميات",
|
||||
"DESCRIPTION": "التسميات توفر طريقة أسهل لتصنيف محادثتك. إنشاء بعض التسميات مثل #كبار_العملاء، #العملاء_المحتملون الخ، بحيث يمكنك استخدامها في محادثة لاحقا.",
|
||||
"NEW_LINK": "انقر هنا لإنشاء وسوم"
|
||||
},
|
||||
"CANNED_RESPONSES": {
|
||||
"TITLE": "Create canned responses",
|
||||
"DESCRIPTION": "Pre-written quick reply templates help you quickly respond to a conversation. Agents can type the '/' character followed by the shortcode to insert a response.",
|
||||
"NEW_LINK": "Click here to create a canned response"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_SIDEBAR": {
|
||||
|
||||
@@ -241,25 +241,29 @@
|
||||
"ONBOARDING": {
|
||||
"TITLE": "Hey 👋, Welcome to %{installationName}!",
|
||||
"DESCRIPTION": "Thanks for signing up. We want you to get the most out of %{installationName}. Here are a few things you can do in %{installationName} to make the experience delightful.",
|
||||
"GREETING_MORNING": "👋 Good morning, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_AFTERNOON": "👋 Good afternoon, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_EVENING": "👋 Good evening, %{name}. Welcome to %{installationName}.",
|
||||
"READ_LATEST_UPDATES": "Read our latest updates",
|
||||
"ALL_CONVERSATION": {
|
||||
"TITLE": "All your conversations in one place",
|
||||
"DESCRIPTION": "View all the conversations from your customers in one single dashboard. You can filter the conversations by the incoming channel, label and status."
|
||||
"DESCRIPTION": "View all the conversations from your customers in one single dashboard. You can filter the conversations by the incoming channel, label and status.",
|
||||
"NEW_LINK": "Click here to create an inbox"
|
||||
},
|
||||
"TEAM_MEMBERS": {
|
||||
"TITLE": "Invite your team members",
|
||||
"DESCRIPTION": "Since you are getting ready to talk to your customer, bring in your teammates to assist you. You can invite your teammates by adding their email addresses to the agent list.",
|
||||
"NEW_LINK": "Click here to invite a team member"
|
||||
},
|
||||
"INBOXES": {
|
||||
"TITLE": "Connect Inboxes",
|
||||
"DESCRIPTION": "Connect various channels through which your customers would be talking to you. It can be a website live-chat, your Facebook or Twitter page or even your WhatsApp number.",
|
||||
"NEW_LINK": "Click here to create an inbox"
|
||||
},
|
||||
"LABELS": {
|
||||
"TITLE": "Organize conversations with labels",
|
||||
"DESCRIPTION": "Labels provide an easier way to categorize your conversation. Create some labels like #support-enquiry, #billing-question etc., so that you can use them in a conversation later.",
|
||||
"NEW_LINK": "Click here to create tags"
|
||||
},
|
||||
"CANNED_RESPONSES": {
|
||||
"TITLE": "Create canned responses",
|
||||
"DESCRIPTION": "Pre-written quick reply templates help you quickly respond to a conversation. Agents can type the '/' character followed by the shortcode to insert a response.",
|
||||
"NEW_LINK": "Click here to create a canned response"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_SIDEBAR": {
|
||||
|
||||
@@ -241,25 +241,29 @@
|
||||
"ONBOARDING": {
|
||||
"TITLE": "Hola 👋, Benvingut a %{installationName}!",
|
||||
"DESCRIPTION": "Gràcies per registrar-te. Volem que treguis el màxim profit de %{installationName}. Aquí teniu algunes coses que pots fer a %{installationName} perquè l'experiència sigui agradable.",
|
||||
"GREETING_MORNING": "👋 Good morning, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_AFTERNOON": "👋 Good afternoon, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_EVENING": "👋 Good evening, %{name}. Welcome to %{installationName}.",
|
||||
"READ_LATEST_UPDATES": "Llegiu les nostres últimes actualitzacions",
|
||||
"ALL_CONVERSATION": {
|
||||
"TITLE": "Totes les teves converses en un sol lloc",
|
||||
"DESCRIPTION": "Consulta totes les converses dels teus clients en un únic tauler. Podeu filtrar les converses pel canal d'entrada, l'etiqueta i l'estat."
|
||||
"DESCRIPTION": "Consulta totes les converses dels teus clients en un únic tauler. Podeu filtrar les converses pel canal d'entrada, l'etiqueta i l'estat.",
|
||||
"NEW_LINK": "Fes clic aquí per crear una safata d'entrada"
|
||||
},
|
||||
"TEAM_MEMBERS": {
|
||||
"TITLE": "Convida els membres del teu equip",
|
||||
"DESCRIPTION": "Com que us esteu preparant per parlar amb el vostre client, feu venir els vostres companys d'equip per ajudar-vos. Pots convidar els teus companys d'equip afegint les seves adreces de correu electrònic a la llista d'agents.",
|
||||
"NEW_LINK": "Feu clic aquí per convidar un membre de l'equip"
|
||||
},
|
||||
"INBOXES": {
|
||||
"TITLE": "Connecta les safates d'entrada",
|
||||
"DESCRIPTION": "Connect various channels through which your customers would be talking to you. It can be a website live-chat, your Facebook or Twitter page or even your WhatsApp number.",
|
||||
"NEW_LINK": "Fes clic aquí per crear una safata d'entrada"
|
||||
},
|
||||
"LABELS": {
|
||||
"TITLE": "Organitza converses amb etiquetes",
|
||||
"DESCRIPTION": "Les etiquetes proporcionen una manera més fàcil de classificar la vostra conversa. Crea algunes etiquetes com #consulta-suport, #pregunta-facturació, etc., perquè les puguis utilitzar en una conversa més endavant.",
|
||||
"NEW_LINK": "Fes clic aquí per crear etiquetes"
|
||||
},
|
||||
"CANNED_RESPONSES": {
|
||||
"TITLE": "Create canned responses",
|
||||
"DESCRIPTION": "Pre-written quick reply templates help you quickly respond to a conversation. Agents can type the '/' character followed by the shortcode to insert a response.",
|
||||
"NEW_LINK": "Click here to create a canned response"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_SIDEBAR": {
|
||||
|
||||
@@ -241,25 +241,29 @@
|
||||
"ONBOARDING": {
|
||||
"TITLE": "Ahoj, 👋, Vítejte na %{installationName}!",
|
||||
"DESCRIPTION": "Děkujeme za registraci. Chceme, abyste získali maximum z %{installationName}. Zde je několik věcí, které můžete v %{installationName} udělat, abyste udělali zážitek příjemný.",
|
||||
"GREETING_MORNING": "👋 Good morning, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_AFTERNOON": "👋 Good afternoon, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_EVENING": "👋 Good evening, %{name}. Welcome to %{installationName}.",
|
||||
"READ_LATEST_UPDATES": "Přečtěte si nejnovější aktualizace",
|
||||
"ALL_CONVERSATION": {
|
||||
"TITLE": "Všechny vaše konverzace na jednom místě",
|
||||
"DESCRIPTION": "Zobrazit všechny konverzace od zákazníků na jednom nástěnce. Můžete filtrovat konverzace podle příchozího kanálu, popisku a stavu."
|
||||
"DESCRIPTION": "Zobrazit všechny konverzace od zákazníků na jednom nástěnce. Můžete filtrovat konverzace podle příchozího kanálu, popisku a stavu.",
|
||||
"NEW_LINK": "Klikněte zde pro vytvoření schránky"
|
||||
},
|
||||
"TEAM_MEMBERS": {
|
||||
"TITLE": "Pozvěte své členy týmu",
|
||||
"DESCRIPTION": "Vzhledem k tomu, že se připravujete na rozhovor se zákazníkem, přiveďte své týmové spolupracovníky, kteří Vám pomohou. Můžete pozvat své spolupracovníky přidáním jejich e-mailové adresy do seznamu agentů.",
|
||||
"NEW_LINK": "Klikněte zde pro pozvání člena týmu"
|
||||
},
|
||||
"INBOXES": {
|
||||
"TITLE": "Připojit schránky",
|
||||
"DESCRIPTION": "Připojte různé kanály, pomocí kterých budou Vaši zákazníci s Vámi mluvit. Může to být živý chat, Vaše Facebook nebo Twitter stránka nebo dokonce Vaše WhatsApp číslo.",
|
||||
"NEW_LINK": "Klikněte zde pro vytvoření schránky"
|
||||
},
|
||||
"LABELS": {
|
||||
"TITLE": "Spravovat konverzace s popisky",
|
||||
"DESCRIPTION": "Štítky poskytují jednodušší způsob, jak kategorizovat vaši konverzaci. Vytvořte nějaké štítky jako #podpora, #fakturace atd., abyste je mohli použít v konverzaci později.",
|
||||
"NEW_LINK": "Klikněte zde pro vytvoření štítků"
|
||||
},
|
||||
"CANNED_RESPONSES": {
|
||||
"TITLE": "Create canned responses",
|
||||
"DESCRIPTION": "Pre-written quick reply templates help you quickly respond to a conversation. Agents can type the '/' character followed by the shortcode to insert a response.",
|
||||
"NEW_LINK": "Click here to create a canned response"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_SIDEBAR": {
|
||||
|
||||
@@ -241,25 +241,29 @@
|
||||
"ONBOARDING": {
|
||||
"TITLE": "Hej 👋, Velkommen til %{installationName}!",
|
||||
"DESCRIPTION": "Tak for din tilmelding. Vi vil have dig til at få mest muligt ud af %{installationName}. Her er et par ting, du kan gøre i %{installationName} for at gøre oplevelsen dejlig.",
|
||||
"GREETING_MORNING": "👋 Good morning, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_AFTERNOON": "👋 Good afternoon, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_EVENING": "👋 Good evening, %{name}. Welcome to %{installationName}.",
|
||||
"READ_LATEST_UPDATES": "Læs vores seneste opdateringer",
|
||||
"ALL_CONVERSATION": {
|
||||
"TITLE": "Alle dine samtaler på ét sted",
|
||||
"DESCRIPTION": "Se alle samtaler fra dine kunder på et enkelt betjeningspanel. Du kan filtrere samtaler efter indgående kanal, etiket og status."
|
||||
"DESCRIPTION": "Se alle samtaler fra dine kunder på et enkelt betjeningspanel. Du kan filtrere samtaler efter indgående kanal, etiket og status.",
|
||||
"NEW_LINK": "Klik her for at oprette en indbakke"
|
||||
},
|
||||
"TEAM_MEMBERS": {
|
||||
"TITLE": "Inviter dine teammedlemmer",
|
||||
"DESCRIPTION": "Da du er ved at blive klar til at tale med din kunde, skal du medbringe dine holdkammerater for at hjælpe dig. Du kan invitere dine holdkammerater ved at tilføje deres e-mailadresser til agentlisten.",
|
||||
"NEW_LINK": "Klik her for at invitere et teammedlem"
|
||||
},
|
||||
"INBOXES": {
|
||||
"TITLE": "Forbind Indbakker",
|
||||
"DESCRIPTION": "Tilslut forskellige kanaler, hvorigennem dine kunder ville tale med dig. Det kan være en hjemmeside live-chat, din Facebook eller Twitter side eller endda dit WhatsApp nummer.",
|
||||
"NEW_LINK": "Klik her for at oprette en indbakke"
|
||||
},
|
||||
"LABELS": {
|
||||
"TITLE": "Organiser samtaler med etiketter",
|
||||
"DESCRIPTION": "Etiketter giver en lettere måde at kategorisere din samtale. Opret nogle etiketter som #support-forespørgsel, #billing-spørgsmål osv., så du kan bruge dem i en samtale senere.",
|
||||
"NEW_LINK": "Klik her for at oprette tags"
|
||||
},
|
||||
"CANNED_RESPONSES": {
|
||||
"TITLE": "Create canned responses",
|
||||
"DESCRIPTION": "Pre-written quick reply templates help you quickly respond to a conversation. Agents can type the '/' character followed by the shortcode to insert a response.",
|
||||
"NEW_LINK": "Click here to create a canned response"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_SIDEBAR": {
|
||||
|
||||
@@ -241,25 +241,29 @@
|
||||
"ONBOARDING": {
|
||||
"TITLE": "Hallo 👋, Willkommen bei %{installationName}!",
|
||||
"DESCRIPTION": "Danke für's Registrieren. Wir möchten, dass Sie %{installationName} optimal nutzen. Hier sind ein paar Dinge, die Sie in %{installationName} tun können, um das Erlebnis angenehm zu gestalten.",
|
||||
"GREETING_MORNING": "👋 Good morning, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_AFTERNOON": "👋 Good afternoon, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_EVENING": "👋 Good evening, %{name}. Welcome to %{installationName}.",
|
||||
"READ_LATEST_UPDATES": "Lesen Sie unsere neuesten Updates",
|
||||
"ALL_CONVERSATION": {
|
||||
"TITLE": "All Ihre Konversationen an einem Ort",
|
||||
"DESCRIPTION": "Zeigen Sie alle Konversationen Ihrer Kunden in einem einzigen Dashboard an. Sie können die Konversationen nach eingehendem Kanal, Bezeichnung und Status filtern."
|
||||
"DESCRIPTION": "Zeigen Sie alle Konversationen Ihrer Kunden in einem einzigen Dashboard an. Sie können die Konversationen nach eingehendem Kanal, Bezeichnung und Status filtern.",
|
||||
"NEW_LINK": "Hier klicken, um einen Posteingang zu erstellen"
|
||||
},
|
||||
"TEAM_MEMBERS": {
|
||||
"TITLE": "Laden Sie Ihre Teammitglieder ein",
|
||||
"DESCRIPTION": "Da Sie sich darauf vorbereiten, mit Ihrem Kunden zu sprechen, ziehen Sie Ihre Teamkollegen hinzu, um Ihnen zu helfen. Sie können Ihre Teamkollegen einladen, indem Sie deren E-Mail-Adressen zur Agentenliste hinzufügen.",
|
||||
"NEW_LINK": "Klicken Sie hier, um ein Teammitglied einzuladen"
|
||||
},
|
||||
"INBOXES": {
|
||||
"TITLE": "Posteingänge verbinden",
|
||||
"DESCRIPTION": "Verbinden Sie verschiedene Kanäle, über die Ihre Kunden mit Ihnen sprechen würden. Das kann ein Website-Live-Chat, Ihre Facebook- oder Twitter-Seite oder sogar Ihre WhatsApp-Nummer sein.",
|
||||
"NEW_LINK": "Hier klicken, um einen Posteingang zu erstellen"
|
||||
},
|
||||
"LABELS": {
|
||||
"TITLE": "Organisieren Sie Konversationen mit Labels",
|
||||
"DESCRIPTION": "Labels bieten eine einfachere Möglichkeit, Ihre Konversation zu kategorisieren. Erstellen Sie einige Labels wie #support-enquiry, #billing-quest etc., damit Sie sie später in einem Gespräch verwenden können.",
|
||||
"NEW_LINK": "Hier klicken, um Tags zu erstellen"
|
||||
},
|
||||
"CANNED_RESPONSES": {
|
||||
"TITLE": "Create canned responses",
|
||||
"DESCRIPTION": "Pre-written quick reply templates help you quickly respond to a conversation. Agents can type the '/' character followed by the shortcode to insert a response.",
|
||||
"NEW_LINK": "Click here to create a canned response"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_SIDEBAR": {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"PASSWORD_UPDATE_SUCCESS": "Ihr Passwort wurde erfolgreich geändert",
|
||||
"AFTER_EMAIL_CHANGED": "Ihr Profil wurde erfolgreich aktualisiert. Melden Sie sich erneut an, wenn Ihre Anmeldeinformationen geändert werden",
|
||||
"FORM": {
|
||||
"PICTURE": "Profile Picture",
|
||||
"PICTURE": "Profilbild",
|
||||
"AVATAR": "Profilbild",
|
||||
"ERROR": "Bitte korrigieren Sie Formularfehler",
|
||||
"REMOVE_IMAGE": "Entfernen",
|
||||
@@ -37,12 +37,12 @@
|
||||
},
|
||||
"MESSAGE_SIGNATURE_SECTION": {
|
||||
"TITLE": "Persönliche Nachrichtensignatur",
|
||||
"NOTE": "Create a unique message signature to appear at the end of every message you send from any inbox. You can also include an inline image, which is supported in live-chat, email, and API inboxes.",
|
||||
"NOTE": "Erstellen Sie eine einzigartige Nachrichtensignatur, die am Ende jeder Nachricht angezeigt wird, die Sie aus einem beliebigen Posteingang senden. Sie können auch ein Inline-Bild einfügen, das in Live-Chat-, E-Mail- und API-Postfächern unterstützt wird.",
|
||||
"BTN_TEXT": "Nachrichten-Signatur speichern",
|
||||
"API_ERROR": "Signatur konnte nicht gespeichert werden! Versuch es noch einmal",
|
||||
"API_SUCCESS": "Signatur erfolgreich gespeichert",
|
||||
"IMAGE_UPLOAD_ERROR": "Couldn't upload image! Try again",
|
||||
"IMAGE_UPLOAD_SUCCESS": "Image added successfully. Please click on save to save the signature",
|
||||
"IMAGE_UPLOAD_ERROR": "Bild konnte nicht hochgeladen werden! Versuchen Sie es erneut",
|
||||
"IMAGE_UPLOAD_SUCCESS": "Bild erfolgreich hinzugefügt. Bitte klicken Sie auf Speichern, um die Signatur zu speichern",
|
||||
"IMAGE_UPLOAD_SIZE_ERROR": "Bildgröße sollte kleiner als {size}MB sein"
|
||||
},
|
||||
"MESSAGE_SIGNATURE": {
|
||||
@@ -65,11 +65,11 @@
|
||||
"NOTE": "Audio-Benachrichtigungen im Dashboard für neue Nachrichten und Unterhaltungen aktivieren.",
|
||||
"ALERT_TYPES": {
|
||||
"NONE": "Keine",
|
||||
"MINE": "Assigned",
|
||||
"MINE": "Zugewiesen",
|
||||
"ALL": "Alle"
|
||||
},
|
||||
"ALERT_TYPE": {
|
||||
"TITLE": "Alert events for conversations:",
|
||||
"TITLE": "Ereignisse für Konversationen zu alarmieren:",
|
||||
"NONE": "Keine",
|
||||
"ASSIGNED": "Zugewiesene Unterhaltungen",
|
||||
"ALL_CONVERSATIONS": "Alle Konversationen"
|
||||
@@ -91,26 +91,26 @@
|
||||
"CONVERSATION_MENTION": "Senden Sie E-Mail-Benachrichtigungen, wenn Sie in einer Konversation erwähnt werden",
|
||||
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "Senden Sie E-Mail-Benachrichtigungen, wenn eine neue Nachricht in einer zugewiesenen Konversation erstellt wird",
|
||||
"PARTICIPATING_CONVERSATION_NEW_MESSAGE": "E-Mail-Benachrichtigungen senden, wenn eine neue Nachricht in einer zugewiesenen Konversation erstellt wird",
|
||||
"SLA_MISSED_FIRST_RESPONSE": "Send email notifications when a conversation misses first response SLA",
|
||||
"SLA_MISSED_NEXT_RESPONSE": "Send email notifications when a conversation misses next response SLA",
|
||||
"SLA_MISSED_RESOLUTION": "Send email notifications when a conversation misses resolution SLA"
|
||||
"SLA_MISSED_FIRST_RESPONSE": "Senden Sie E-Mail-Benachrichtigungen, wenn eine Konversation die erste Antwort-SLA verpasst",
|
||||
"SLA_MISSED_NEXT_RESPONSE": "Senden Sie E-Mail-Benachrichtigungen, wenn eine Konversation die nächste Antwort-SLA verpasst",
|
||||
"SLA_MISSED_RESOLUTION": "Senden Sie E-Mail-Benachrichtigungen, wenn eine Konversation die Lösung-SLA verpasst"
|
||||
},
|
||||
"NOTIFICATIONS": {
|
||||
"TITLE": "Notification preferences",
|
||||
"TYPE_TITLE": "Notification type",
|
||||
"TITLE": "Benachrichtigungseinstellungen",
|
||||
"TYPE_TITLE": "Benachrichtigungsart",
|
||||
"EMAIL": "E-Mail",
|
||||
"PUSH": "Push notification",
|
||||
"PUSH": "Push-Benachrichtigung",
|
||||
"TYPES": {
|
||||
"CONVERSATION_CREATED": "A new conversation is created",
|
||||
"CONVERSATION_ASSIGNED": "A conversation is assigned to you",
|
||||
"CONVERSATION_MENTION": "You are mentioned in a conversation",
|
||||
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "A new message is created in an assigned conversation",
|
||||
"PARTICIPATING_CONVERSATION_NEW_MESSAGE": "A new message is created in a participating conversation",
|
||||
"SLA_MISSED_FIRST_RESPONSE": "A conversation misses first response SLA",
|
||||
"SLA_MISSED_NEXT_RESPONSE": "A conversation misses next response SLA",
|
||||
"SLA_MISSED_RESOLUTION": "A conversation misses resolution SLA"
|
||||
"CONVERSATION_CREATED": "Eine neue Konversation wurde erstellt",
|
||||
"CONVERSATION_ASSIGNED": "Eine Konversation wurde Ihnen zugewiesen",
|
||||
"CONVERSATION_MENTION": "Sie wurden in einer Konversation erwähnt",
|
||||
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "Eine neue Nachricht wurde in einer zugewiesenen Konversation erstellt",
|
||||
"PARTICIPATING_CONVERSATION_NEW_MESSAGE": "Eine neue Nachricht wurde in einer Konversation erstellt, an der Sie teilnehmen",
|
||||
"SLA_MISSED_FIRST_RESPONSE": "Eine Konversation verpasst die erste Antwort-SLA",
|
||||
"SLA_MISSED_NEXT_RESPONSE": "Eine Konversation verpasst die nächste Antwort-SLA",
|
||||
"SLA_MISSED_RESOLUTION": "Eine Konversation verpasst die Lösungs-SLA"
|
||||
},
|
||||
"BROWSER_PERMISSION": "Enable push notifications for your browser so you’re able to receive them"
|
||||
"BROWSER_PERMISSION": "Aktivieren Sie Push-Benachrichtigungen für Ihren Browser, damit Sie diese empfangen können"
|
||||
},
|
||||
"API": {
|
||||
"UPDATE_SUCCESS": "Ihre Benachrichtigungseinstellungen wurden erfolgreich aktualisiert",
|
||||
@@ -126,9 +126,9 @@
|
||||
"PARTICIPATING_CONVERSATION_NEW_MESSAGE": "Sende Push-Benachrichtigungen, wenn eine neue Nachricht in einer zugewiesenen Konversation erstellt wird",
|
||||
"HAS_ENABLED_PUSH": "Sie haben die Push-Benachrichtigung für diesen Browser aktiviert.",
|
||||
"REQUEST_PUSH": "Push-Benachrichtigungen aktivieren",
|
||||
"SLA_MISSED_FIRST_RESPONSE": "Send push notifications when a conversation misses first response SLA",
|
||||
"SLA_MISSED_NEXT_RESPONSE": "Send push notifications when a conversation misses next response SLA",
|
||||
"SLA_MISSED_RESOLUTION": "Send push notifications when a conversation misses resolution SLA"
|
||||
"SLA_MISSED_FIRST_RESPONSE": "Senden Sie Push-Benachrichtigungen, wenn eine Konversation die erste Antwort-SLA verpasst",
|
||||
"SLA_MISSED_NEXT_RESPONSE": "Senden Sie Push-Benachrichtigungen, wenn eine Konversation die nächste Antwort-SLA verpasst",
|
||||
"SLA_MISSED_RESOLUTION": "Senden Sie Push-Benachrichtigungen, wenn eine Konversation die Lösungs-SLA verpasst"
|
||||
},
|
||||
"PROFILE_IMAGE": {
|
||||
"LABEL": "Profilbild"
|
||||
@@ -229,7 +229,7 @@
|
||||
"SIDEBAR": {
|
||||
"CURRENTLY_VIEWING_ACCOUNT": "Wird gerade angezeigt:",
|
||||
"SWITCH": "Wechseln",
|
||||
"INBOX_VIEW": "Inbox View",
|
||||
"INBOX_VIEW": "Posteingangsansicht",
|
||||
"CONVERSATIONS": "Gespräche",
|
||||
"INBOX": "Posteingang",
|
||||
"ALL_CONVERSATIONS": "Alle Konversationen",
|
||||
@@ -239,7 +239,7 @@
|
||||
"REPORTS": "Berichte",
|
||||
"SETTINGS": "Einstellungen",
|
||||
"CONTACTS": "Kontakte",
|
||||
"CAPTAIN": "Captain",
|
||||
"CAPTAIN": "Kapitän",
|
||||
"HOME": "Hauptseite",
|
||||
"AGENTS": "Agenten",
|
||||
"AGENT_BOTS": "Bots",
|
||||
@@ -279,7 +279,7 @@
|
||||
"SLA": "SLA",
|
||||
"BETA": "Beta",
|
||||
"REPORTS_OVERVIEW": "Übersicht",
|
||||
"REAUTHORIZE": "Your inbox connection has expired, please reconnect\n to continue receiving and sending messages",
|
||||
"REAUTHORIZE": "Ihre Posteingangsverbindung ist abgelaufen. Bitte erneut verbinden,\n um weiterhin Nachrichten empfangen und senden zu können",
|
||||
"HELP_CENTER": {
|
||||
"TITLE": "Hilfezentrum",
|
||||
"ALL_ARTICLES": "Alle Artikel",
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
"TEAMS_SETTINGS": {
|
||||
"NEW_TEAM": "Neues Team erstellen",
|
||||
"HEADER": "Teams",
|
||||
"LOADING": "Fetching teams",
|
||||
"DESCRIPTION": "Teams allow you to organize agents into groups based on their responsibilities. An agent can belong to multiple teams. When working collaboratively, you can assign conversations to specific teams.",
|
||||
"LEARN_MORE": "Learn more about teams",
|
||||
"LOADING": "Teams werden abgerufen",
|
||||
"DESCRIPTION": "Teams ermöglichen es Ihnen, Agenten basierend auf ihren Aufgaben in Gruppen zu organisieren. Ein Agent kann mehreren Teams angehören. Bei der Zusammenarbeit können Sie Unterhaltungen bestimmten Teams zuweisen.",
|
||||
"LEARN_MORE": "Erfahren Sie mehr über Teams",
|
||||
"LIST": {
|
||||
"404": "Es wurden noch keine Teams auf diesem Konto erstellt.",
|
||||
"EDIT_TEAM": "Team bearbeiten",
|
||||
@@ -100,7 +100,7 @@
|
||||
"ERROR_MESSAGE": "Das Team konnte nicht gelöscht werden. Versuchen Sie es erneut."
|
||||
},
|
||||
"CONFIRM": {
|
||||
"TITLE": "Are you sure you want to delete the team?",
|
||||
"TITLE": "Sind Sie sicher, dass Sie das Team löschen möchten?",
|
||||
"PLACE_HOLDER": "Bitte geben Sie {teamName} zur Bestätigung ein",
|
||||
"MESSAGE": "Das Löschen des Teams wird die Teamzuordnung aus den Konversationen entfernen, die diesem Team zugewiesen wurden.",
|
||||
"YES": "Löschen ",
|
||||
|
||||
@@ -241,25 +241,29 @@
|
||||
"ONBOARDING": {
|
||||
"TITLE": "Γεια σας 👋, Καλώς ήρθατε στο %{installationName}!",
|
||||
"DESCRIPTION": "Ευχαριστούμε για την εγγραφή. Θέλουμε να αξιοποιήσετε στο έπακρο το %{installationName}. Εδώ είναι μερικά πράγματα που μπορείτε να κάνετε στο %{installationName} για να έχετε μια ευχάριστη εμπειρία.",
|
||||
"GREETING_MORNING": "👋 Good morning, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_AFTERNOON": "👋 Good afternoon, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_EVENING": "👋 Good evening, %{name}. Welcome to %{installationName}.",
|
||||
"READ_LATEST_UPDATES": "Διαβάστε τις πρόσφατες ενημερώσεις μας",
|
||||
"ALL_CONVERSATION": {
|
||||
"TITLE": "Όλες οι συνομιλίες σας σε ένα μέρος",
|
||||
"DESCRIPTION": "Δείτε όλες τις συνομιλίες από τους πελάτες σας σε ένα μόνο ταμπλό. Μπορείτε να φιλτράρετε τις συνομιλίες κατά εισερχόμενο κανάλι, ετικέτα και κατάσταση."
|
||||
"DESCRIPTION": "Δείτε όλες τις συνομιλίες από τους πελάτες σας σε ένα μόνο ταμπλό. Μπορείτε να φιλτράρετε τις συνομιλίες κατά εισερχόμενο κανάλι, ετικέτα και κατάσταση.",
|
||||
"NEW_LINK": "Κάντε κλικ εδώ για δημιουργία εισερχόμενων"
|
||||
},
|
||||
"TEAM_MEMBERS": {
|
||||
"TITLE": "Προσκαλέστε τα μέλη της ομάδας σας",
|
||||
"DESCRIPTION": "Δεδομένου ότι ετοιμάζεστε να συνομιλήσετε με τον πελάτη σας, φέρτε τους συνάδελφους σας για να σας βοηθήσουν. Μπορείτε να προσκαλέσετε τους συνάδελφους σας προσθέτοντας τη διεύθυνση email τους στη λίστα αντιπροσώπων.",
|
||||
"NEW_LINK": "Κάντε κλικ εδώ για να καλέσετε ένα μέλος της ομάδας"
|
||||
},
|
||||
"INBOXES": {
|
||||
"TITLE": "Σύνδεση Εισερχομένων",
|
||||
"DESCRIPTION": "Συνδέστε διάφορα κανάλια μέσω των οποίων οι πελάτες σας θα μιλούν μαζί σας. Μπορεί να είναι μια ιστοσελίδα live-chat, το Facebook ή το Twitter σελίδα σας ή ακόμα και ο αριθμός σας WhatsApp.",
|
||||
"NEW_LINK": "Κάντε κλικ εδώ για δημιουργία εισερχόμενων"
|
||||
},
|
||||
"LABELS": {
|
||||
"TITLE": "Οργάνωση συνομιλιών με ετικέτες",
|
||||
"DESCRIPTION": "Οι ετικέτες παρέχουν έναν ευκολότερο τρόπο για να κατηγοριοποιήσετε τη συνομιλία σας. Δημιουργήστε μερικές ετικέτες όπως το #support-quiry, #billing-question κλπ., έτσι ώστε να μπορείτε να τις χρησιμοποιήσετε σε μια συζήτηση αργότερα.",
|
||||
"NEW_LINK": "Κάντε κλικ εδώ για δημιουργία ετικετών (tags)"
|
||||
},
|
||||
"CANNED_RESPONSES": {
|
||||
"TITLE": "Create canned responses",
|
||||
"DESCRIPTION": "Pre-written quick reply templates help you quickly respond to a conversation. Agents can type the '/' character followed by the shortcode to insert a response.",
|
||||
"NEW_LINK": "Click here to create a canned response"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_SIDEBAR": {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"AUTOMATION": {
|
||||
"HEADER": "Automations",
|
||||
"HEADER": "Automation",
|
||||
"DESCRIPTION": "Automation can replace and streamline existing processes that require manual effort, such as adding labels and assigning conversations to the most suitable agent. This allows the team to focus on their strengths while reducing time spent on routine tasks.",
|
||||
"LEARN_MORE": "Learn more about automation",
|
||||
"HEADER_BTN_TXT": "Add Automation Rule",
|
||||
"LOADING": "Fetching automation rules",
|
||||
"SIDEBAR_TXT": "<p><b>Automation Rules</b> <p>Automation can replace and automate existing processes that require manual effort. You can do many things with automation, including adding labels and assigning conversation to the best agent. So the team focuses on what they do best and spends more little time on manual tasks.</p>",
|
||||
"ADD": {
|
||||
"TITLE": "Add Automation Rule",
|
||||
"SUBMIT": "Create",
|
||||
@@ -39,7 +40,12 @@
|
||||
}
|
||||
},
|
||||
"LIST": {
|
||||
"TABLE_HEADER": ["Name", "Description", "Active", "Created on"],
|
||||
"TABLE_HEADER": [
|
||||
"Name",
|
||||
"Description",
|
||||
"Active",
|
||||
"Created on"
|
||||
],
|
||||
"404": "No automation rules found"
|
||||
},
|
||||
"DELETE": {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"MACROS": {
|
||||
"HEADER": "Macros",
|
||||
"DESCRIPTION": "A macro is a set of saved actions that help customer service agents easily complete tasks. The agents can define a set of actions like tagging a conversation with a label, sending an email transcript, updating a custom attribute, etc., and they can run these actions in a single click.",
|
||||
"LEARN_MORE": "Learn more about macros",
|
||||
"HEADER_BTN_TXT": "Add a new macro",
|
||||
"HEADER_BTN_TXT_SAVE": "Save macro",
|
||||
"LOADING": "Fetching macros",
|
||||
"SIDEBAR_TXT": "<p><b>Macros</b><p>A macro is a set of saved actions that help customer service agents easily complete tasks. The agents can define a set of actions like tagging a conversation with a label, sending an email transcript, updating a custom attribute, etc., and they can run these actions in a single click. When the agents run the macro, the actions would be performed sequentially in the order they are defined. Macros improve productivity and increase consistency in actions. </p><p>A macro can be helpful in 2 ways. </p><p><b>As an agent assist:</b> If an agent performs a set of actions multiple times, they can save it as a macro and execute all the actions together using a single click.</p><p><b>As an option to onboard a team member:</b> Every agent has to perform many different checks/actions during each conversation. Onboarding a new support team member will be easy if pre-defined macros are available on the account. Instead of describing each step in detail, the manager/team lead can point to the macros used in different scenarios.</p>",
|
||||
"ERROR": "Something went wrong. Please try again",
|
||||
"ORDER_INFO": "Macros will run in the order you add your actions. You can rearrange them by dragging them by the handle beside each node.",
|
||||
"ADD": {
|
||||
@@ -24,7 +25,12 @@
|
||||
}
|
||||
},
|
||||
"LIST": {
|
||||
"TABLE_HEADER": ["Name", "Created by", "Last updated by", "Visibility"],
|
||||
"TABLE_HEADER": [
|
||||
"Name",
|
||||
"Created by",
|
||||
"Last updated by",
|
||||
"Visibility"
|
||||
],
|
||||
"404": "No macros found"
|
||||
},
|
||||
"DELETE": {
|
||||
|
||||
@@ -241,25 +241,29 @@
|
||||
"ONBOARDING": {
|
||||
"TITLE": "Hola 👋, ¡Bienvenido a %{installationName}!",
|
||||
"DESCRIPTION": "Gracias por registrarse. Queremos que saque el máximo provecho de %{installationName}. Aquí hay algunas cosas que puede hacer en %{installationName} para hacer que la experiencia sea agradable.",
|
||||
"GREETING_MORNING": "👋 Good morning, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_AFTERNOON": "👋 Good afternoon, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_EVENING": "👋 Good evening, %{name}. Welcome to %{installationName}.",
|
||||
"READ_LATEST_UPDATES": "Leer nuestras últimas actualizaciones",
|
||||
"ALL_CONVERSATION": {
|
||||
"TITLE": "Todas sus conversaciones en un solo lugar",
|
||||
"DESCRIPTION": "Ver todas las conversaciones de sus clientes en un solo panel de control. Puede filtrar las conversaciones por el canal entrante, etiqueta y estado."
|
||||
"DESCRIPTION": "Ver todas las conversaciones de sus clientes en un solo panel de control. Puede filtrar las conversaciones por el canal entrante, etiqueta y estado.",
|
||||
"NEW_LINK": "Haga clic aquí para crear una bandeja de entrada"
|
||||
},
|
||||
"TEAM_MEMBERS": {
|
||||
"TITLE": "Invite a los miembros de su equipo",
|
||||
"DESCRIPTION": "Ya que usted se está preparando para hablar con su cliente, traiga a sus compañeros para asistirle. Puedes invitar a sus compañeros de equipo añadiendo su dirección de correo electrónico a la lista de agentes.",
|
||||
"NEW_LINK": "Haga clic aquí para invitar a un miembro del equipo"
|
||||
},
|
||||
"INBOXES": {
|
||||
"TITLE": "Conectar bandejas de entrada",
|
||||
"DESCRIPTION": "Conecte varios canales a través de los cuales sus clientes le hablarían. Puede ser un sitio web en vivo, su página de Facebook o Twitter o incluso su número de WhatsApp.",
|
||||
"NEW_LINK": "Haga clic aquí para crear una bandeja de entrada"
|
||||
},
|
||||
"LABELS": {
|
||||
"TITLE": "Organice las conversaciones con etiquetas",
|
||||
"DESCRIPTION": "Las etiquetas proporcionan una forma fácil de clasificar su conversación. Cree algunas etiquetas como #pregunta-soporte, #pregunta-dacturación etc., para que pueda usarlas en una conversación más tarde.",
|
||||
"NEW_LINK": "Haga clic aquí para crear etiquetas"
|
||||
},
|
||||
"CANNED_RESPONSES": {
|
||||
"TITLE": "Create canned responses",
|
||||
"DESCRIPTION": "Pre-written quick reply templates help you quickly respond to a conversation. Agents can type the '/' character followed by the shortcode to insert a response.",
|
||||
"NEW_LINK": "Click here to create a canned response"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_SIDEBAR": {
|
||||
|
||||
@@ -241,25 +241,29 @@
|
||||
"ONBOARDING": {
|
||||
"TITLE": "سلام 👋، خوش آمدی به %{installationName}!",
|
||||
"DESCRIPTION": "از این که ثبت نام کرده اید سپاسگذاریم. ما ماخواهیم بهترین تجربه را از %{installationName} داشته باشید. در اینجا چند کار وجود دارد که می توانید در %{installationName} انجام دهید تا تجربه بهتری داشته باشید.",
|
||||
"GREETING_MORNING": "👋 Good morning, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_AFTERNOON": "👋 Good afternoon, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_EVENING": "👋 Good evening, %{name}. Welcome to %{installationName}.",
|
||||
"READ_LATEST_UPDATES": "آخرین به روزرسانی های ما را بخوانید",
|
||||
"ALL_CONVERSATION": {
|
||||
"TITLE": "همه مکالمات شما در یک مکان",
|
||||
"DESCRIPTION": "همه مکالمات مشتریان خود را در یک داشبورد واحد مشاهده کنید. می توانید مکالمات را بر اساس کانال ، برچسب و وضعیت ورودی فیلتر کنید."
|
||||
"DESCRIPTION": "همه مکالمات مشتریان خود را در یک داشبورد واحد مشاهده کنید. می توانید مکالمات را بر اساس کانال ، برچسب و وضعیت ورودی فیلتر کنید.",
|
||||
"NEW_LINK": "برای ایجاد صندوق ورودی اینجا را کلیک کنید"
|
||||
},
|
||||
"TEAM_MEMBERS": {
|
||||
"TITLE": "اعضای تیم خود را دعوت کنید",
|
||||
"DESCRIPTION": "از آنجا که در حال آماده شدن برای گفتگو با مشتری هستید ، می توانید از هم تیمی های خود نیز کمک بگیرید. با افزودن آدرس ایمیل آنها به لیست اپراتور ها، می توانید از هم تیمی های خود دعوت کنید.",
|
||||
"NEW_LINK": "برای دعوت از یکی از اعضای تیم اینجا را کلیک کنید"
|
||||
},
|
||||
"INBOXES": {
|
||||
"TITLE": "صندوق ورودی را وصل کنید",
|
||||
"DESCRIPTION": "شما می توانید کانال های مختلفی را برای ارتباط با مشتری ایجاد نمایید، شما میتوانید از طریق چت داخل سایت یا فیس بوک یا توییتر و حتی واتس آپ استفاده نمایید.",
|
||||
"NEW_LINK": "برای ایجاد صندوق ورودی اینجا را کلیک کنید"
|
||||
},
|
||||
"LABELS": {
|
||||
"TITLE": "سازماندهی مکالمات با برچسب ها",
|
||||
"DESCRIPTION": "برچسب ها روش ساده تری برای دسته بندی مکالمه شما فراهم می کنند. برخی از برچسب ها مانند #پشتیبانی-درخواست ، #صورتحساب و غیره را ایجاد کنید تا بعداً بتوانید از آنها در مکالمه استفاده کنید.",
|
||||
"NEW_LINK": "برای ایجاد برچسب ها اینجا را کلیک کنید"
|
||||
},
|
||||
"CANNED_RESPONSES": {
|
||||
"TITLE": "Create canned responses",
|
||||
"DESCRIPTION": "Pre-written quick reply templates help you quickly respond to a conversation. Agents can type the '/' character followed by the shortcode to insert a response.",
|
||||
"NEW_LINK": "Click here to create a canned response"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_SIDEBAR": {
|
||||
|
||||
@@ -241,25 +241,29 @@
|
||||
"ONBOARDING": {
|
||||
"TITLE": "Hey 👋, Welcome to %{installationName}!",
|
||||
"DESCRIPTION": "Thanks for signing up. We want you to get the most out of %{installationName}. Here are a few things you can do in %{installationName} to make the experience delightful.",
|
||||
"GREETING_MORNING": "👋 Good morning, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_AFTERNOON": "👋 Good afternoon, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_EVENING": "👋 Good evening, %{name}. Welcome to %{installationName}.",
|
||||
"READ_LATEST_UPDATES": "Read our latest updates",
|
||||
"ALL_CONVERSATION": {
|
||||
"TITLE": "All your conversations in one place",
|
||||
"DESCRIPTION": "View all the conversations from your customers in one single dashboard. You can filter the conversations by the incoming channel, label and status."
|
||||
"DESCRIPTION": "View all the conversations from your customers in one single dashboard. You can filter the conversations by the incoming channel, label and status.",
|
||||
"NEW_LINK": "Click here to create an inbox"
|
||||
},
|
||||
"TEAM_MEMBERS": {
|
||||
"TITLE": "Invite your team members",
|
||||
"DESCRIPTION": "Since you are getting ready to talk to your customer, bring in your teammates to assist you. You can invite your teammates by adding their email addresses to the agent list.",
|
||||
"NEW_LINK": "Click here to invite a team member"
|
||||
},
|
||||
"INBOXES": {
|
||||
"TITLE": "Connect Inboxes",
|
||||
"DESCRIPTION": "Connect various channels through which your customers would be talking to you. It can be a website live-chat, your Facebook or Twitter page or even your WhatsApp number.",
|
||||
"NEW_LINK": "Click here to create an inbox"
|
||||
},
|
||||
"LABELS": {
|
||||
"TITLE": "Organize conversations with labels",
|
||||
"DESCRIPTION": "Labels provide an easier way to categorize your conversation. Create some labels like #support-enquiry, #billing-question etc., so that you can use them in a conversation later.",
|
||||
"NEW_LINK": "Click here to create tags"
|
||||
},
|
||||
"CANNED_RESPONSES": {
|
||||
"TITLE": "Create canned responses",
|
||||
"DESCRIPTION": "Pre-written quick reply templates help you quickly respond to a conversation. Agents can type the '/' character followed by the shortcode to insert a response.",
|
||||
"NEW_LINK": "Click here to create a canned response"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_SIDEBAR": {
|
||||
|
||||
@@ -241,25 +241,29 @@
|
||||
"ONBOARDING": {
|
||||
"TITLE": "Salut 👋, Bienvenue sur %{installationName}!",
|
||||
"DESCRIPTION": "Merci pour votre inscription. Nous souhaitons que vous tiriez le meilleur parti de %{installationName}. Voici quelques actions que vous pouvez effectuer dans %{installationName} pour rendre votre expérience agréable.",
|
||||
"GREETING_MORNING": "👋 Good morning, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_AFTERNOON": "👋 Good afternoon, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_EVENING": "👋 Good evening, %{name}. Welcome to %{installationName}.",
|
||||
"READ_LATEST_UPDATES": "Consultez nos dernières mises à jour",
|
||||
"ALL_CONVERSATION": {
|
||||
"TITLE": "Toutes vos conversations en un seul lieu",
|
||||
"DESCRIPTION": "Visualisez toutes les conversations de vos clients dans un seul tableau de bord. Vous pouvez filtrer les conversations par le canal entrant, l'étiquette et le statut."
|
||||
"DESCRIPTION": "Visualisez toutes les conversations de vos clients dans un seul tableau de bord. Vous pouvez filtrer les conversations par le canal entrant, l'étiquette et le statut.",
|
||||
"NEW_LINK": "Cliquez ici pour créer une boîte de réception"
|
||||
},
|
||||
"TEAM_MEMBERS": {
|
||||
"TITLE": "Invitez les membres de votre équipe",
|
||||
"DESCRIPTION": "Puisque vous vous apprêtez à parler à votre client, amenez vos coéquipiers pour vous aider. Vous pouvez inviter vos coéquipiers en ajoutant leurs adresses e-mail à la liste des agents.",
|
||||
"NEW_LINK": "Cliquez ici pour inviter un membre de l'équipe"
|
||||
},
|
||||
"INBOXES": {
|
||||
"TITLE": "Connecter les boîtes de réception",
|
||||
"DESCRIPTION": "Connectez différents canaux à travers lesquels vos clients vous parleraient. Il peut s'agir d'un chat de site internet, de votre page Facebook ou Twitter ou même de votre numéro WhatsApp.",
|
||||
"NEW_LINK": "Cliquez ici pour créer une boîte de réception"
|
||||
},
|
||||
"LABELS": {
|
||||
"TITLE": "Organiser les conversations avec des labels",
|
||||
"DESCRIPTION": "Les labels fournissent un moyen plus facile de catégoriser votre conversation. Créez des étiquettes comme #demande-support, #question-facturation etc., afin que vous puissiez les utiliser dans une conversation plus tard.",
|
||||
"NEW_LINK": "Cliquez ici pour créer des tags"
|
||||
},
|
||||
"CANNED_RESPONSES": {
|
||||
"TITLE": "Create canned responses",
|
||||
"DESCRIPTION": "Pre-written quick reply templates help you quickly respond to a conversation. Agents can type the '/' character followed by the shortcode to insert a response.",
|
||||
"NEW_LINK": "Click here to create a canned response"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_SIDEBAR": {
|
||||
|
||||
@@ -241,25 +241,29 @@
|
||||
"ONBOARDING": {
|
||||
"TITLE": "היי 👋, ברוך הבא ל%{installationName}!",
|
||||
"DESCRIPTION": "תודה על ההרשמה. אנחנו רוצים לתת לך את הכי הרבה מ %{installationName}. הינה כמה דברים שאתה יכול לעשות עם %{installationName} לחוויה טובה.",
|
||||
"GREETING_MORNING": "👋 Good morning, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_AFTERNOON": "👋 Good afternoon, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_EVENING": "👋 Good evening, %{name}. Welcome to %{installationName}.",
|
||||
"READ_LATEST_UPDATES": "תקרא את העדכונים אחרונים",
|
||||
"ALL_CONVERSATION": {
|
||||
"TITLE": "כל השיחות שלך במקום אחד",
|
||||
"DESCRIPTION": "צפה בכל השיחות שלך עם הלקוחות בדאשבורד אחד. אתה יכול לסנן את השיחות לפי הערוץ, התווית והסטטוס."
|
||||
"DESCRIPTION": "צפה בכל השיחות שלך עם הלקוחות בדאשבורד אחד. אתה יכול לסנן את השיחות לפי הערוץ, התווית והסטטוס.",
|
||||
"NEW_LINK": "לחץ כאן כדי ליצור תיבה"
|
||||
},
|
||||
"TEAM_MEMBERS": {
|
||||
"TITLE": "הזמן את חברי הצוות שלך",
|
||||
"DESCRIPTION": "מאחר ואתם מוכנים לדבר עם הלקוח, הביאו את חברי הצוות שלכם לעזור לכם. תוכלו להזמין חברי צוות על ידי הוספה של כתובת הדוא\"ל שלהם לרשימת הסוכנים.",
|
||||
"NEW_LINK": "לחץ כאן כדי להזמין חבר צוות"
|
||||
},
|
||||
"INBOXES": {
|
||||
"TITLE": "התחבר לתיבות",
|
||||
"DESCRIPTION": "חבר ערוצים שונים דרכם הלקוחות שלך היו מדברים איתך. זה יכול להיות אתר צ'אט חי, דף הפייסבוק או הטוויטר שלך או אפילו מספר הוואטסאפ שלך.",
|
||||
"NEW_LINK": "לחץ כאן כדי ליצור תיבה"
|
||||
},
|
||||
"LABELS": {
|
||||
"TITLE": "ארגן שיחות עם תוויות",
|
||||
"DESCRIPTION": "תוויות מספקות דרך קלה יותר לסווג את השיחה שלך. צור כמה תוויות כמו #support-enquiry, #billing-question וכו', כדי שתוכל להשתמש בהן בשיחה מאוחר יותר.",
|
||||
"NEW_LINK": "לחץ כאן כדי ליצור תגיות"
|
||||
},
|
||||
"CANNED_RESPONSES": {
|
||||
"TITLE": "Create canned responses",
|
||||
"DESCRIPTION": "Pre-written quick reply templates help you quickly respond to a conversation. Agents can type the '/' character followed by the shortcode to insert a response.",
|
||||
"NEW_LINK": "Click here to create a canned response"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_SIDEBAR": {
|
||||
|
||||
@@ -241,25 +241,29 @@
|
||||
"ONBOARDING": {
|
||||
"TITLE": "Hey 👋, Welcome to %{installationName}!",
|
||||
"DESCRIPTION": "Thanks for signing up. We want you to get the most out of %{installationName}. Here are a few things you can do in %{installationName} to make the experience delightful.",
|
||||
"GREETING_MORNING": "👋 Good morning, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_AFTERNOON": "👋 Good afternoon, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_EVENING": "👋 Good evening, %{name}. Welcome to %{installationName}.",
|
||||
"READ_LATEST_UPDATES": "Read our latest updates",
|
||||
"ALL_CONVERSATION": {
|
||||
"TITLE": "All your conversations in one place",
|
||||
"DESCRIPTION": "View all the conversations from your customers in one single dashboard. You can filter the conversations by the incoming channel, label and status."
|
||||
"DESCRIPTION": "View all the conversations from your customers in one single dashboard. You can filter the conversations by the incoming channel, label and status.",
|
||||
"NEW_LINK": "Click here to create an inbox"
|
||||
},
|
||||
"TEAM_MEMBERS": {
|
||||
"TITLE": "Invite your team members",
|
||||
"DESCRIPTION": "Since you are getting ready to talk to your customer, bring in your teammates to assist you. You can invite your teammates by adding their email addresses to the agent list.",
|
||||
"NEW_LINK": "Click here to invite a team member"
|
||||
},
|
||||
"INBOXES": {
|
||||
"TITLE": "Connect Inboxes",
|
||||
"DESCRIPTION": "Connect various channels through which your customers would be talking to you. It can be a website live-chat, your Facebook or Twitter page or even your WhatsApp number.",
|
||||
"NEW_LINK": "Click here to create an inbox"
|
||||
},
|
||||
"LABELS": {
|
||||
"TITLE": "Organize conversations with labels",
|
||||
"DESCRIPTION": "Labels provide an easier way to categorize your conversation. Create some labels like #support-enquiry, #billing-question etc., so that you can use them in a conversation later.",
|
||||
"NEW_LINK": "Click here to create tags"
|
||||
},
|
||||
"CANNED_RESPONSES": {
|
||||
"TITLE": "Create canned responses",
|
||||
"DESCRIPTION": "Pre-written quick reply templates help you quickly respond to a conversation. Agents can type the '/' character followed by the shortcode to insert a response.",
|
||||
"NEW_LINK": "Click here to create a canned response"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_SIDEBAR": {
|
||||
|
||||
@@ -367,12 +367,19 @@
|
||||
"DESCRIPTION": "Click on the Sign in with Microsoft button to get started. You will redirected to the email sign in page. Once you accept the requested permissions, you would be redirected back to the inbox creation step.",
|
||||
"EMAIL_PLACEHOLDER": "Enter email address",
|
||||
"SIGN_IN": "Sign in with Microsoft",
|
||||
"HELP": "To add your Microsoft account as a channel, you need to authenticate your Microsoft account by clicking on 'Sign in with Microsoft' ",
|
||||
"ERROR_MESSAGE": "There was an error connecting to Microsoft, please try again"
|
||||
},
|
||||
"GOOGLE": {
|
||||
"TITLE": "Google Email",
|
||||
"DESCRIPTION": "Click on the Sign in with Google button to get started. You will redirected to the email sign in page. Once you accept the requested permissions, you would be redirected back to the inbox creation step.",
|
||||
"SIGN_IN": "Sign in with Google",
|
||||
"EMAIL_PLACEHOLDER": "Enter email address",
|
||||
"ERROR_MESSAGE": "There was an error connecting to Google, please try again"
|
||||
}
|
||||
},
|
||||
"DETAILS": {
|
||||
"LOADING_FB": "Authenticating you with Facebook...",
|
||||
"ERROR_FB_LOADING": "Error loading Facebook SDK. Please disable any ad-blockers and try again from a different browser.",
|
||||
"ERROR_FB_AUTH": "Something went wrong, Please refresh page...",
|
||||
"ERROR_FB_UNAUTHORIZED": "You're not authorized to perform this action. ",
|
||||
"ERROR_FB_UNAUTHORIZED_HELP": "Please ensure you have access to the Facebook page with full control. You can read more about Facebook roles <a href=\" https://www.facebook.com/help/187316341316631\">here</a>.",
|
||||
@@ -736,6 +743,7 @@
|
||||
},
|
||||
"EMAIL_PROVIDERS": {
|
||||
"MICROSOFT": "Microsoft",
|
||||
"GOOGLE": "Google",
|
||||
"OTHER_PROVIDERS": "Other Providers"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,12 +106,12 @@
|
||||
"DEACTIVATION_SUCCESFUL": "Automation Rule Deactivated Successfully",
|
||||
"ACTIVATION_ERROR": "Could not Activate Automation, Please try again later",
|
||||
"DEACTIVATION_ERROR": "Could not Deactivate Automation, Please try again later",
|
||||
"CONFIRMATION_LABEL": "Yes",
|
||||
"CANCEL_LABEL": "No"
|
||||
"CONFIRMATION_LABEL": "Da",
|
||||
"CANCEL_LABEL": "Ne"
|
||||
},
|
||||
"ATTACHMENT": {
|
||||
"UPLOAD_ERROR": "Could not upload attachment, Please try again",
|
||||
"LABEL_IDLE": "Upload Attachment",
|
||||
"LABEL_IDLE": "Prenesi privitak",
|
||||
"LABEL_UPLOADING": "Prenosim...",
|
||||
"LABEL_UPLOADED": "Successfully Uploaded",
|
||||
"LABEL_UPLOAD_FAILED": "Upload Failed"
|
||||
|
||||
@@ -241,25 +241,29 @@
|
||||
"ONBOARDING": {
|
||||
"TITLE": "Hey 👋, Welcome to %{installationName}!",
|
||||
"DESCRIPTION": "Thanks for signing up. We want you to get the most out of %{installationName}. Here are a few things you can do in %{installationName} to make the experience delightful.",
|
||||
"GREETING_MORNING": "👋 Good morning, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_AFTERNOON": "👋 Good afternoon, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_EVENING": "👋 Good evening, %{name}. Welcome to %{installationName}.",
|
||||
"READ_LATEST_UPDATES": "Read our latest updates",
|
||||
"ALL_CONVERSATION": {
|
||||
"TITLE": "All your conversations in one place",
|
||||
"DESCRIPTION": "View all the conversations from your customers in one single dashboard. You can filter the conversations by the incoming channel, label and status."
|
||||
"DESCRIPTION": "View all the conversations from your customers in one single dashboard. You can filter the conversations by the incoming channel, label and status.",
|
||||
"NEW_LINK": "Click here to create an inbox"
|
||||
},
|
||||
"TEAM_MEMBERS": {
|
||||
"TITLE": "Invite your team members",
|
||||
"DESCRIPTION": "Since you are getting ready to talk to your customer, bring in your teammates to assist you. You can invite your teammates by adding their email addresses to the agent list.",
|
||||
"NEW_LINK": "Click here to invite a team member"
|
||||
},
|
||||
"INBOXES": {
|
||||
"TITLE": "Connect Inboxes",
|
||||
"DESCRIPTION": "Connect various channels through which your customers would be talking to you. It can be a website live-chat, your Facebook or Twitter page or even your WhatsApp number.",
|
||||
"NEW_LINK": "Click here to create an inbox"
|
||||
},
|
||||
"LABELS": {
|
||||
"TITLE": "Organize conversations with labels",
|
||||
"DESCRIPTION": "Labels provide an easier way to categorize your conversation. Create some labels like #support-enquiry, #billing-question etc., so that you can use them in a conversation later.",
|
||||
"NEW_LINK": "Click here to create tags"
|
||||
},
|
||||
"CANNED_RESPONSES": {
|
||||
"TITLE": "Create canned responses",
|
||||
"DESCRIPTION": "Pre-written quick reply templates help you quickly respond to a conversation. Agents can type the '/' character followed by the shortcode to insert a response.",
|
||||
"NEW_LINK": "Click here to create a canned response"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_SIDEBAR": {
|
||||
|
||||
@@ -241,25 +241,29 @@
|
||||
"ONBOARDING": {
|
||||
"TITLE": "Hello 👋, köszöntünk a %{installationName}!",
|
||||
"DESCRIPTION": "Köszönjük, hogy feliratkoztál. Segíteni akarunk, hogy a legtöbbet hozd ki a %{installationName}. Itt találsz egy pár dolgot, amit a %{installationName} lehetővé tesz.",
|
||||
"GREETING_MORNING": "👋 Good morning, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_AFTERNOON": "👋 Good afternoon, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_EVENING": "👋 Good evening, %{name}. Welcome to %{installationName}.",
|
||||
"READ_LATEST_UPDATES": "Olvasd el legfrissebbeket",
|
||||
"ALL_CONVERSATION": {
|
||||
"TITLE": "Minden beszélgetésed egy helyen",
|
||||
"DESCRIPTION": "Kövesd az összes ügyfélbeszélgetésedet egy felületről. Szűrheted a beszélgetéseket csatorna, cimke vagy státusz alapján."
|
||||
"DESCRIPTION": "Kövesd az összes ügyfélbeszélgetésedet egy felületről. Szűrheted a beszélgetéseket csatorna, cimke vagy státusz alapján.",
|
||||
"NEW_LINK": "Kattints ide postaláda létrehozásához"
|
||||
},
|
||||
"TEAM_MEMBERS": {
|
||||
"TITLE": "Hívd meg csapattagjaidat",
|
||||
"DESCRIPTION": "Mivel éppen ügyféllel készülsz beszélni, hívd meg a csapattagjaidat, hogy segíthessenek neked. Az e-mailcímük ügynöklistába való megadásával tudod meghívni őket.",
|
||||
"NEW_LINK": "Kattints ide csapattag meghívásához"
|
||||
},
|
||||
"INBOXES": {
|
||||
"TITLE": "Inboxok összekötése",
|
||||
"DESCRIPTION": "Különböző csatornákat használva kapcsolódj az ügyfeleidhez. Weboldal chateden, a Facebook vagy Twitter fiókodon, vagy akár WhatsApp számodon keresztül.",
|
||||
"NEW_LINK": "Kattints ide postaláda létrehozásához"
|
||||
},
|
||||
"LABELS": {
|
||||
"TITLE": "Rendezd a beszélgetéseket cimkékkel",
|
||||
"DESCRIPTION": "A cimkék egyszerű módot biztosítanak hogy kategorizáld a beszélgetést. Hozz létre cimkéket, mint például a #támogatás vagy a #számlázás, stb... későbbi használatra.",
|
||||
"NEW_LINK": "Kattints ide cimkék létrehozásához"
|
||||
},
|
||||
"CANNED_RESPONSES": {
|
||||
"TITLE": "Create canned responses",
|
||||
"DESCRIPTION": "Pre-written quick reply templates help you quickly respond to a conversation. Agents can type the '/' character followed by the shortcode to insert a response.",
|
||||
"NEW_LINK": "Click here to create a canned response"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_SIDEBAR": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user