Compare commits
43
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3a5cd759a | ||
|
|
a76cd7684a | ||
|
|
081af45c1e | ||
|
|
3b45fb250c | ||
|
|
2c17c95eab | ||
|
|
bb74c621b5 | ||
|
|
185a122ec7 | ||
|
|
05dece826d | ||
|
|
528b984a8d | ||
|
|
db0e654c03 | ||
|
|
e99e6a8443 | ||
|
|
d6fd3ce048 | ||
|
|
8a2f652b94 | ||
|
|
a3732c8f51 | ||
|
|
1bea121dc5 | ||
|
|
ad0e210aed | ||
|
|
3a0e68030a | ||
|
|
f087461abc | ||
|
|
3a47b7e3d1 | ||
|
|
6dda1e8c8f | ||
|
|
098825c149 | ||
|
|
31e7663258 | ||
|
|
27c7d74006 | ||
|
|
4b5880eb30 | ||
|
|
3b5f5b41ad | ||
|
|
fe5670832a | ||
|
|
bc6420019f | ||
|
|
f82ec3b885 | ||
|
|
32c25047c4 | ||
|
|
deeb580fee | ||
|
|
6eddcdad54 | ||
|
|
7f8d718da3 | ||
|
|
afb0e85312 | ||
|
|
3489783cb8 | ||
|
|
53d68868c6 | ||
|
|
b61ad6e41a | ||
|
|
41c5e7d3f1 | ||
|
|
abc511d00f | ||
|
|
a48f98de9d | ||
|
|
c697964110 | ||
|
|
53216766b8 | ||
|
|
a0153daca4 | ||
|
|
eb9adb4810 |
@@ -96,12 +96,12 @@ gem 'koala'
|
||||
# slack client
|
||||
gem 'slack-ruby-client', '~> 2.2.0'
|
||||
# for dialogflow integrations
|
||||
gem 'google-cloud-dialogflow-v2'
|
||||
gem 'google-cloud-dialogflow-v2', '>= 0.24.0'
|
||||
gem 'grpc'
|
||||
# Translate integrations
|
||||
# 'google-cloud-translate' gem depends on faraday 2.0 version
|
||||
# this dependency breaks the slack-ruby-client gem
|
||||
gem 'google-cloud-translate-v3'
|
||||
gem 'google-cloud-translate-v3', '>= 0.7.0'
|
||||
|
||||
##-- apm and error monitoring ---#
|
||||
# loaded only when environment variables are set.
|
||||
@@ -165,7 +165,7 @@ gem 'audited', '~> 5.4', '>= 5.4.1'
|
||||
|
||||
# need for google auth
|
||||
gem 'omniauth', '>= 2.1.2'
|
||||
gem 'omniauth-google-oauth2', '>= 1.1.2'
|
||||
gem 'omniauth-google-oauth2', '>= 1.1.3'
|
||||
gem 'omniauth-rails_csrf_protection', '~> 1.0', '>= 1.0.2'
|
||||
|
||||
## Gems for reponse bot
|
||||
@@ -200,7 +200,7 @@ group :development do
|
||||
gem 'rack-mini-profiler', '>= 3.2.0', require: false
|
||||
gem 'stackprof'
|
||||
# Should install the associated chrome extension to view query logs
|
||||
gem 'meta_request', '>= 0.8.0'
|
||||
gem 'meta_request', '>= 0.8.3'
|
||||
end
|
||||
|
||||
group :test do
|
||||
|
||||
+19
-19
@@ -257,7 +257,7 @@ GEM
|
||||
faraday-net_http_persistent (2.1.0)
|
||||
faraday (~> 2.5)
|
||||
net-http-persistent (~> 4.0)
|
||||
faraday-retry (2.1.0)
|
||||
faraday-retry (2.2.1)
|
||||
faraday (~> 2.0)
|
||||
fcm (1.0.8)
|
||||
faraday (>= 1.0.0, < 3.0)
|
||||
@@ -271,7 +271,7 @@ GEM
|
||||
fugit (1.11.1)
|
||||
et-orbi (~> 1, >= 1.2.11)
|
||||
raabro (~> 1.4)
|
||||
gapic-common (0.18.0)
|
||||
gapic-common (0.20.0)
|
||||
faraday (>= 1.9, < 3.a)
|
||||
faraday-retry (>= 1.0, < 3.a)
|
||||
google-protobuf (~> 3.14)
|
||||
@@ -301,15 +301,15 @@ GEM
|
||||
google-cloud-core (1.6.0)
|
||||
google-cloud-env (~> 1.0)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-dialogflow-v2 (0.23.0)
|
||||
gapic-common (>= 0.18.0, < 2.a)
|
||||
google-cloud-dialogflow-v2 (0.31.0)
|
||||
gapic-common (>= 0.20.0, < 2.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-location (>= 0.4, < 2.a)
|
||||
google-cloud-env (1.6.0)
|
||||
faraday (>= 0.17.3, < 3.0)
|
||||
google-cloud-errors (1.3.1)
|
||||
google-cloud-location (0.4.0)
|
||||
gapic-common (>= 0.17.1, < 2.a)
|
||||
google-cloud-location (0.6.0)
|
||||
gapic-common (>= 0.20.0, < 2.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-storage (1.44.0)
|
||||
addressable (~> 2.8)
|
||||
@@ -319,17 +319,17 @@ GEM
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
mini_mime (~> 1.0)
|
||||
google-cloud-translate-v3 (0.6.0)
|
||||
gapic-common (>= 0.17.1, < 2.a)
|
||||
google-cloud-translate-v3 (0.10.0)
|
||||
gapic-common (>= 0.20.0, < 2.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-protobuf (3.25.3)
|
||||
google-protobuf (3.25.3-arm64-darwin)
|
||||
google-protobuf (3.25.3-x86_64-darwin)
|
||||
google-protobuf (3.25.3-x86_64-linux)
|
||||
googleapis-common-protos (1.4.0)
|
||||
google-protobuf (~> 3.14)
|
||||
googleapis-common-protos-types (~> 1.2)
|
||||
grpc (~> 1.27)
|
||||
googleapis-common-protos (1.6.0)
|
||||
google-protobuf (>= 3.18, < 5.a)
|
||||
googleapis-common-protos-types (~> 1.7)
|
||||
grpc (~> 1.41)
|
||||
googleapis-common-protos-types (1.14.0)
|
||||
google-protobuf (~> 3.18)
|
||||
googleauth (1.5.2)
|
||||
@@ -457,7 +457,7 @@ GEM
|
||||
marcel (1.0.4)
|
||||
maxminddb (0.1.22)
|
||||
memoist (0.16.2)
|
||||
meta_request (0.8.2)
|
||||
meta_request (0.8.3)
|
||||
rack-contrib (>= 1.1, < 3)
|
||||
railties (>= 3.0.0, < 8)
|
||||
method_source (1.1.0)
|
||||
@@ -522,7 +522,7 @@ GEM
|
||||
hashie (>= 3.4.6)
|
||||
rack (>= 2.2.3)
|
||||
rack-protection
|
||||
omniauth-google-oauth2 (1.1.2)
|
||||
omniauth-google-oauth2 (1.1.3)
|
||||
jwt (>= 2.0)
|
||||
oauth2 (~> 2.0)
|
||||
omniauth (~> 2.0)
|
||||
@@ -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)
|
||||
@@ -881,9 +881,9 @@ DEPENDENCIES
|
||||
foreman
|
||||
geocoder
|
||||
gmail_xoauth
|
||||
google-cloud-dialogflow-v2
|
||||
google-cloud-dialogflow-v2 (>= 0.24.0)
|
||||
google-cloud-storage
|
||||
google-cloud-translate-v3
|
||||
google-cloud-translate-v3 (>= 0.7.0)
|
||||
groupdate
|
||||
grpc
|
||||
haikunator
|
||||
@@ -903,14 +903,14 @@ DEPENDENCIES
|
||||
listen
|
||||
lograge (~> 0.14.0)
|
||||
maxminddb
|
||||
meta_request (>= 0.8.0)
|
||||
meta_request (>= 0.8.3)
|
||||
mock_redis
|
||||
neighbor
|
||||
net-smtp (~> 0.3.4)
|
||||
newrelic-sidekiq-metrics (>= 1.6.2)
|
||||
newrelic_rpm
|
||||
omniauth (>= 2.1.2)
|
||||
omniauth-google-oauth2 (>= 1.1.2)
|
||||
omniauth-google-oauth2 (>= 1.1.3)
|
||||
omniauth-oauth2
|
||||
omniauth-rails_csrf_protection (~> 1.0, >= 1.0.2)
|
||||
pg
|
||||
|
||||
@@ -32,6 +32,8 @@ 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?
|
||||
true
|
||||
@@ -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', '')
|
||||
return [] if domains.blank?
|
||||
|
||||
domains.split("\n").map(&:strip)
|
||||
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')
|
||||
|
||||
@@ -12,7 +12,12 @@ class Api::V1::Accounts::Integrations::HooksController < Api::V1::Accounts::Base
|
||||
|
||||
def process_event
|
||||
response = @hook.process_event(params[:event])
|
||||
if response[:error]
|
||||
|
||||
# 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] }
|
||||
|
||||
@@ -1,13 +1,68 @@
|
||||
class Api::V1::Accounts::UploadController < Api::V1::Accounts::BaseController
|
||||
def create
|
||||
file_blob = ActiveStorage::Blob.create_and_upload!(
|
||||
key: nil,
|
||||
io: params[:attachment].tempfile,
|
||||
filename: params[:attachment].original_filename,
|
||||
content_type: params[:attachment].content_type
|
||||
)
|
||||
file_blob.save!
|
||||
result = if params[:attachment].present?
|
||||
create_from_file
|
||||
elsif params[:external_url].present?
|
||||
create_from_url
|
||||
else
|
||||
render_error('No file or URL provided', :unprocessable_entity)
|
||||
end
|
||||
|
||||
render_success(result) if result.is_a?(ActiveStorage::Blob)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_from_file
|
||||
attachment = params[:attachment]
|
||||
create_and_save_blob(attachment.tempfile, attachment.original_filename, attachment.content_type)
|
||||
end
|
||||
|
||||
def create_from_url
|
||||
uri = parse_uri(params[:external_url])
|
||||
return if performed?
|
||||
|
||||
fetch_and_process_file_from_uri(uri)
|
||||
end
|
||||
|
||||
def parse_uri(url)
|
||||
uri = URI.parse(url)
|
||||
validate_uri(uri)
|
||||
uri
|
||||
rescue URI::InvalidURIError, SocketError
|
||||
render_error('Invalid URL provided', :unprocessable_entity)
|
||||
nil
|
||||
end
|
||||
|
||||
def validate_uri(uri)
|
||||
raise URI::InvalidURIError unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
|
||||
end
|
||||
|
||||
def fetch_and_process_file_from_uri(uri)
|
||||
uri.open do |file|
|
||||
create_and_save_blob(file, File.basename(uri.path), file.content_type)
|
||||
end
|
||||
rescue OpenURI::HTTPError => e
|
||||
render_error("Failed to fetch file from URL: #{e.message}", :unprocessable_entity)
|
||||
rescue SocketError
|
||||
render_error('Invalid URL provided', :unprocessable_entity)
|
||||
rescue StandardError
|
||||
render_error('An unexpected error occurred', :internal_server_error)
|
||||
end
|
||||
|
||||
def create_and_save_blob(io, filename, content_type)
|
||||
ActiveStorage::Blob.create_and_upload!(
|
||||
io: io,
|
||||
filename: filename,
|
||||
content_type: content_type
|
||||
)
|
||||
end
|
||||
|
||||
def render_success(file_blob)
|
||||
render json: { file_url: url_for(file_blob), blob_key: file_blob.key, blob_id: file_blob.id }
|
||||
end
|
||||
|
||||
def render_error(message, status)
|
||||
render json: { error: message }, status: status
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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'
|
||||
@@ -109,35 +117,41 @@ export default {
|
||||
lastConversationIndex,
|
||||
};
|
||||
};
|
||||
const handlePreviousConversation = () => {
|
||||
const { allConversations, activeConversationIndex } =
|
||||
getKeyboardListenerParams();
|
||||
if (activeConversationIndex === -1) {
|
||||
allConversations[0].click();
|
||||
}
|
||||
if (activeConversationIndex >= 1) {
|
||||
allConversations[activeConversationIndex - 1].click();
|
||||
}
|
||||
};
|
||||
const handleNextConversation = () => {
|
||||
const handleConversationNavigation = direction => {
|
||||
const {
|
||||
allConversations,
|
||||
activeConversationIndex,
|
||||
lastConversationIndex,
|
||||
} = getKeyboardListenerParams();
|
||||
if (activeConversationIndex === -1) {
|
||||
allConversations[lastConversationIndex].click();
|
||||
} else if (activeConversationIndex < lastConversationIndex) {
|
||||
allConversations[activeConversationIndex + 1].click();
|
||||
|
||||
// Determine the new index based on the direction
|
||||
const newIndex =
|
||||
direction === 'previous'
|
||||
? activeConversationIndex - 1
|
||||
: activeConversationIndex + 1;
|
||||
|
||||
// Check if the new index is within the valid range
|
||||
if (
|
||||
allConversations.length > 0 &&
|
||||
newIndex >= 0 &&
|
||||
newIndex <= lastConversationIndex
|
||||
) {
|
||||
// Click the conversation at the new index
|
||||
allConversations[newIndex].click();
|
||||
} else if (allConversations.length > 0) {
|
||||
// If the new index is out of range, click the first or last conversation based on the direction
|
||||
const fallbackIndex =
|
||||
direction === 'previous' ? 0 : lastConversationIndex;
|
||||
allConversations[fallbackIndex].click();
|
||||
}
|
||||
};
|
||||
const keyboardEvents = {
|
||||
'Alt+KeyJ': {
|
||||
action: () => handlePreviousConversation(),
|
||||
action: () => handleConversationNavigation('previous'),
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
'Alt+KeyK': {
|
||||
action: () => handleNextConversation(),
|
||||
action: () => handleConversationNavigation('next'),
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
@@ -146,6 +160,9 @@ export default {
|
||||
return {
|
||||
uiSettings,
|
||||
conversationListRef,
|
||||
setFilterAttributes,
|
||||
initializeStatusAndAssigneeFilterToModal,
|
||||
initializeInboxTeamAndLabelFilterToModal,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
@@ -860,6 +877,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>
|
||||
|
||||
@@ -331,10 +331,12 @@ export default {
|
||||
::v-deep {
|
||||
.selector-wrap {
|
||||
@apply m-0 top-1;
|
||||
|
||||
.selector-name {
|
||||
@apply ml-0;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
@apply ml-0;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ 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();
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
import { useAI } from 'dashboard/composables/useAI';
|
||||
import AICTAModal from './AICTAModal.vue';
|
||||
import AIAssistanceModal from './AIAssistanceModal.vue';
|
||||
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 {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<script>
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import { useAI } from 'dashboard/composables/useAI';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import AILoader from './AILoader.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AILoader,
|
||||
},
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
aiOption: {
|
||||
type: String,
|
||||
@@ -15,12 +14,9 @@ export default {
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
const { draftMessage, processEvent, recordAnalytics } = useAI();
|
||||
return {
|
||||
draftMessage,
|
||||
processEvent,
|
||||
recordAnalytics,
|
||||
};
|
||||
return { draftMessage, processEvent, recordAnalytics, formatMessage };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -1,38 +1,34 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
text: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 120,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showMore: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
textToBeDisplayed() {
|
||||
if (this.showMore || this.text.length <= this.limit) {
|
||||
return this.text;
|
||||
}
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
|
||||
return this.text.slice(0, this.limit) + '...';
|
||||
},
|
||||
buttonLabel() {
|
||||
const i18nKey = !this.showMore ? 'SHOW_MORE' : 'SHOW_LESS';
|
||||
return this.$t(`COMPONENTS.SHOW_MORE_BLOCK.${i18nKey}`);
|
||||
},
|
||||
const props = defineProps({
|
||||
text: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
methods: {
|
||||
toggleShowMore() {
|
||||
this.showMore = !this.showMore;
|
||||
},
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 120,
|
||||
},
|
||||
});
|
||||
const { t } = useI18n();
|
||||
const showMore = ref(false);
|
||||
|
||||
const textToBeDisplayed = computed(() => {
|
||||
if (showMore.value || props.text.length <= props.limit) {
|
||||
return props.text;
|
||||
}
|
||||
|
||||
return props.text.slice(0, props.limit) + '...';
|
||||
});
|
||||
const buttonLabel = computed(() => {
|
||||
const i18nKey = !showMore.value ? 'SHOW_MORE' : 'SHOW_LESS';
|
||||
return t(`COMPONENTS.SHOW_MORE_BLOCK.${i18nKey}`);
|
||||
});
|
||||
|
||||
const toggleShowMore = () => {
|
||||
showMore.value = !showMore.value;
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -41,16 +37,10 @@ export default {
|
||||
{{ textToBeDisplayed }}
|
||||
<button
|
||||
v-if="text.length > limit"
|
||||
class="show-more--button"
|
||||
class="text-woot-500 !p-0 !border-0 align-top"
|
||||
@click="toggleShowMore"
|
||||
>
|
||||
{{ buttonLabel }}
|
||||
</button>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.show-more--button {
|
||||
color: var(--w-500);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -17,6 +17,8 @@ import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import TagAgents from '../conversation/TagAgents.vue';
|
||||
import CannedResponse from '../conversation/CannedResponse.vue';
|
||||
import VariableList from '../conversation/VariableList.vue';
|
||||
import KeyboardEmojiSelector from './keyboardEmojiSelector.vue';
|
||||
|
||||
import {
|
||||
appendSignature,
|
||||
removeSignature,
|
||||
@@ -24,6 +26,7 @@ import {
|
||||
scrollCursorIntoView,
|
||||
findNodeToInsertImage,
|
||||
setURLWithQueryAndSize,
|
||||
getContentNode,
|
||||
} from 'dashboard/helper/editorHelper';
|
||||
|
||||
const TYPING_INDICATOR_IDLE_TIME = 4000;
|
||||
@@ -35,10 +38,8 @@ import {
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import {
|
||||
replaceVariablesInMessage,
|
||||
createTypingIndicator,
|
||||
} from '@chatwoot/utils';
|
||||
|
||||
import { createTypingIndicator } from '@chatwoot/utils';
|
||||
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
||||
import { uploadFile } from 'dashboard/helper/uploadHelper';
|
||||
@@ -71,7 +72,12 @@ const createState = (
|
||||
|
||||
export default {
|
||||
name: 'WootMessageEditor',
|
||||
components: { TagAgents, CannedResponse, VariableList },
|
||||
components: {
|
||||
TagAgents,
|
||||
CannedResponse,
|
||||
VariableList,
|
||||
KeyboardEmojiSelector,
|
||||
},
|
||||
mixins: [keyboardEventListenerMixins],
|
||||
props: {
|
||||
value: { type: String, default: '' },
|
||||
@@ -119,9 +125,11 @@ export default {
|
||||
showUserMentions: false,
|
||||
showCannedMenu: false,
|
||||
showVariables: false,
|
||||
showEmojiMenu: false,
|
||||
mentionSearchKey: '',
|
||||
cannedSearchTerm: '',
|
||||
variableSearchTerm: '',
|
||||
emojiSearchTerm: '',
|
||||
editorView: null,
|
||||
range: null,
|
||||
state: undefined,
|
||||
@@ -169,7 +177,7 @@ export default {
|
||||
this.editorView = args.view;
|
||||
this.range = args.range;
|
||||
|
||||
this.mentionSearchKey = args.text.replace('@', '');
|
||||
this.mentionSearchKey = args.text;
|
||||
|
||||
return false;
|
||||
},
|
||||
@@ -198,7 +206,7 @@ export default {
|
||||
this.editorView = args.view;
|
||||
this.range = args.range;
|
||||
|
||||
this.cannedSearchTerm = args.text.replace('/', '');
|
||||
this.cannedSearchTerm = args.text;
|
||||
return false;
|
||||
},
|
||||
onExit: () => {
|
||||
@@ -226,7 +234,7 @@ export default {
|
||||
this.editorView = args.view;
|
||||
this.range = args.range;
|
||||
|
||||
this.variableSearchTerm = args.text.replace('{{', '');
|
||||
this.variableSearchTerm = args.text;
|
||||
return false;
|
||||
},
|
||||
onExit: () => {
|
||||
@@ -238,6 +246,31 @@ export default {
|
||||
return event.keyCode === 13 && this.showVariables;
|
||||
},
|
||||
}),
|
||||
suggestionsPlugin({
|
||||
matcher: triggerCharacters(':', 1), // Trigger after ':' and at least 1 characters
|
||||
suggestionClass: '',
|
||||
onEnter: args => {
|
||||
this.showEmojiMenu = true;
|
||||
this.emojiSearchTerm = args.text || '';
|
||||
this.range = args.range;
|
||||
this.editorView = args.view;
|
||||
return false;
|
||||
},
|
||||
onChange: args => {
|
||||
this.editorView = args.view;
|
||||
this.range = args.range;
|
||||
this.emojiSearchTerm = args.text;
|
||||
return false;
|
||||
},
|
||||
onExit: () => {
|
||||
this.emojiSearchTerm = '';
|
||||
this.showEmojiMenu = false;
|
||||
return false;
|
||||
},
|
||||
onKeyDown: ({ event }) => {
|
||||
return event.keyCode === 13 && this.showEmojiMenu;
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
sendWithSignature() {
|
||||
@@ -267,6 +300,8 @@ export default {
|
||||
},
|
||||
editorId() {
|
||||
this.showCannedMenu = false;
|
||||
this.showEmojiMenu = false;
|
||||
this.showVariables = false;
|
||||
this.cannedSearchTerm = '';
|
||||
this.reloadState(this.value);
|
||||
},
|
||||
@@ -517,57 +552,36 @@ export default {
|
||||
this.editorView.dispatch(tr.setSelection(selection));
|
||||
this.editorView.focus();
|
||||
},
|
||||
insertMentionNode(mentionItem) {
|
||||
/**
|
||||
* Inserts special content (mention, canned response, variable, emoji) into the editor.
|
||||
* @param {string} type - The type of special content to insert. Possible values: 'mention', 'canned_response', 'variable', 'emoji'.
|
||||
* @param {Object|string} content - The content to insert, depending on the type.
|
||||
*/
|
||||
insertSpecialContent(type, content) {
|
||||
if (!this.editorView) {
|
||||
return null;
|
||||
}
|
||||
const node = this.editorView.state.schema.nodes.mention.create({
|
||||
userId: mentionItem.id,
|
||||
userFullName: mentionItem.name,
|
||||
});
|
||||
|
||||
this.insertNodeIntoEditor(node, this.range.from, this.range.to);
|
||||
this.$track(CONVERSATION_EVENTS.USED_MENTIONS);
|
||||
|
||||
return false;
|
||||
},
|
||||
insertCannedResponse(cannedItem) {
|
||||
const updatedMessage = replaceVariablesInMessage({
|
||||
message: cannedItem,
|
||||
variables: this.variables,
|
||||
});
|
||||
|
||||
if (!this.editorView) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
let node = new MessageMarkdownTransformer(messageSchema).parse(
|
||||
updatedMessage
|
||||
let { node, from, to } = getContentNode(
|
||||
this.editorView,
|
||||
type,
|
||||
content,
|
||||
this.range,
|
||||
this.variables
|
||||
);
|
||||
|
||||
const from =
|
||||
node.textContent === updatedMessage
|
||||
? this.range.from
|
||||
: this.range.from - 1;
|
||||
|
||||
this.insertNodeIntoEditor(node, from, this.range.to);
|
||||
|
||||
this.$track(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE);
|
||||
return false;
|
||||
},
|
||||
insertVariable(variable) {
|
||||
if (!this.editorView) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = `{{${variable}}}`;
|
||||
let node = this.editorView.state.schema.text(content);
|
||||
const { from, to } = this.range;
|
||||
if (!node) return;
|
||||
|
||||
this.insertNodeIntoEditor(node, from, to);
|
||||
this.showVariables = false;
|
||||
this.$track(CONVERSATION_EVENTS.INSERTED_A_VARIABLE);
|
||||
return false;
|
||||
|
||||
const event_map = {
|
||||
mention: CONVERSATION_EVENTS.USED_MENTIONS,
|
||||
cannedResponse: CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE,
|
||||
variable: CONVERSATION_EVENTS.INSERTED_A_VARIABLE,
|
||||
emoji: CONVERSATION_EVENTS.INSERTED_AN_EMOJI,
|
||||
};
|
||||
|
||||
this.$track(event_map[type]);
|
||||
},
|
||||
openFileBrowser() {
|
||||
this.$refs.imageUpload.click();
|
||||
@@ -687,17 +701,22 @@ export default {
|
||||
<TagAgents
|
||||
v-if="showUserMentions && isPrivate"
|
||||
:search-key="mentionSearchKey"
|
||||
@click="insertMentionNode"
|
||||
@click="content => insertSpecialContent('mention', content)"
|
||||
/>
|
||||
<CannedResponse
|
||||
v-if="shouldShowCannedResponses"
|
||||
:search-key="cannedSearchTerm"
|
||||
@click="insertCannedResponse"
|
||||
@click="content => insertSpecialContent('cannedResponse', content)"
|
||||
/>
|
||||
<VariableList
|
||||
v-if="shouldShowVariables"
|
||||
:search-key="variableSearchTerm"
|
||||
@click="insertVariable"
|
||||
@click="content => insertSpecialContent('variable', content)"
|
||||
/>
|
||||
<KeyboardEmojiSelector
|
||||
v-if="showEmojiMenu"
|
||||
:search-key="emojiSearchTerm"
|
||||
@click="emoji => insertSpecialContent('emoji', emoji)"
|
||||
/>
|
||||
<input
|
||||
ref="imageUpload"
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
EditorState,
|
||||
Selection,
|
||||
} from '@chatwoot/prosemirror-schema';
|
||||
import imagePastePlugin from '@chatwoot/prosemirror-schema/src/plugins/image';
|
||||
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
@@ -55,7 +56,7 @@ export default {
|
||||
return {
|
||||
editorView: null,
|
||||
state: undefined,
|
||||
plugins: [],
|
||||
plugins: [imagePastePlugin(this.handleImageUpload)],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -76,6 +77,7 @@ export default {
|
||||
this.reloadState();
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.state = createState(
|
||||
this.value,
|
||||
@@ -95,6 +97,24 @@ export default {
|
||||
openFileBrowser() {
|
||||
this.$refs.imageUploadInput.click();
|
||||
},
|
||||
async handleImageUpload(url) {
|
||||
try {
|
||||
const fileUrl = await this.$store.dispatch(
|
||||
'articles/uploadExternalImage',
|
||||
{
|
||||
portalSlug: this.$route.params.portalSlug,
|
||||
url,
|
||||
}
|
||||
);
|
||||
|
||||
return fileUrl;
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.UN_AUTHORIZED_ERROR')
|
||||
);
|
||||
return '';
|
||||
}
|
||||
},
|
||||
onFileChange() {
|
||||
const file = this.$refs.imageUploadInput.files[0];
|
||||
|
||||
@@ -120,7 +140,6 @@ export default {
|
||||
if (fileUrl) {
|
||||
this.onImageUploadStart(fileUrl);
|
||||
}
|
||||
useAlert(this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.SUCCESS'));
|
||||
} catch (error) {
|
||||
useAlert(this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.ERROR'));
|
||||
}
|
||||
@@ -173,6 +192,18 @@ export default {
|
||||
blur: () => {
|
||||
this.onBlur();
|
||||
},
|
||||
paste: (view, event) => {
|
||||
const data = event.clipboardData.files;
|
||||
if (data.length > 0) {
|
||||
data.forEach(file => {
|
||||
// Check if the file is an image
|
||||
if (file.type.includes('image')) {
|
||||
this.uploadImageToStorage(file);
|
||||
}
|
||||
});
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<script setup>
|
||||
import { shallowRef, computed, onMounted } from 'vue';
|
||||
import emojis from 'shared/components/emoji/emojisGroup.json';
|
||||
import MentionBox from '../mentions/MentionBox.vue';
|
||||
|
||||
const props = defineProps({
|
||||
searchKey: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['click']);
|
||||
|
||||
const allEmojis = shallowRef([]);
|
||||
|
||||
const items = computed(() => {
|
||||
if (!props.searchKey) return [];
|
||||
const searchTerm = props.searchKey.toLowerCase();
|
||||
return allEmojis.value.filter(emoji =>
|
||||
emoji.searchString.includes(searchTerm)
|
||||
);
|
||||
});
|
||||
|
||||
function loadEmojis() {
|
||||
allEmojis.value = emojis.flatMap(group =>
|
||||
group.emojis.map(emoji => ({
|
||||
...emoji,
|
||||
searchString: `${emoji.slug} ${emoji.name}`.toLowerCase(),
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
function handleMentionClick(item = {}) {
|
||||
emit('click', item.emoji);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadEmojis();
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
||||
<template>
|
||||
<MentionBox
|
||||
v-if="items.length"
|
||||
type="emoji"
|
||||
:items="items"
|
||||
@mentionSelect="handleMentionClick"
|
||||
>
|
||||
<template #default="{ item, selected }">
|
||||
<span
|
||||
class="max-w-full inline-flex items-center gap-0.5 min-w-0 mb-0 text-sm font-medium text-slate-900 dark:text-slate-100 group-hover:text-woot-500 dark:group-hover:text-woot-500 truncate"
|
||||
>
|
||||
{{ item.emoji }}
|
||||
<p
|
||||
class="relative mb-0 truncate bottom-px"
|
||||
:class="{
|
||||
'text-woot-500 dark:text-woot-500': selected,
|
||||
'font-normal': !selected,
|
||||
}"
|
||||
>
|
||||
:{{ item.slug }}
|
||||
</p>
|
||||
</span>
|
||||
</template>
|
||||
</MentionBox>
|
||||
</template>
|
||||
@@ -41,14 +41,11 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
||||
<template>
|
||||
<MentionBox
|
||||
v-if="items.length"
|
||||
:items="items"
|
||||
@mentionSelect="handleMentionClick"
|
||||
>
|
||||
<template slot-scope="{ item }">
|
||||
<strong>{{ item.label }}</strong> - {{ item.description }}
|
||||
</template>
|
||||
</MentionBox>
|
||||
/>
|
||||
</template>
|
||||
|
||||
+20
-4
@@ -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 = [];
|
||||
@@ -326,7 +338,11 @@ export default {
|
||||
:show-query-operator="i !== appliedFilters.length - 1"
|
||||
:show-user-input="showUserInput(appliedFilters[i].filter_operator)"
|
||||
grouped-filters
|
||||
:error-message="validationErrors[`filter_${i}`]"
|
||||
:error-message="
|
||||
validationErrors[`filter_${i}`]
|
||||
? $t(`CONTACTS_FILTER.ERRORS.VALUE_REQUIRED`)
|
||||
: ''
|
||||
"
|
||||
@resetFilter="resetFilter(i, appliedFilters[i])"
|
||||
@removeFilter="removeFilter(i)"
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
@@ -243,6 +237,9 @@ export default {
|
||||
if (this.isASmsInbox) {
|
||||
return MESSAGE_MAX_LENGTH.TWILIO_SMS;
|
||||
}
|
||||
if (this.isAnEmailChannel) {
|
||||
return MESSAGE_MAX_LENGTH.EMAIL;
|
||||
}
|
||||
return MESSAGE_MAX_LENGTH.GENERAL;
|
||||
},
|
||||
showFileUpload() {
|
||||
|
||||
@@ -56,20 +56,14 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
||||
<template>
|
||||
<MentionBox
|
||||
v-if="items.length"
|
||||
type="variable"
|
||||
:items="items"
|
||||
@mentionSelect="handleVariableClick"
|
||||
>
|
||||
<template slot-scope="{ item }">
|
||||
<span class="text-capitalize variable--list-label">
|
||||
{{ item.description }}
|
||||
</span>
|
||||
({{ item.label }})
|
||||
</template>
|
||||
</MentionBox>
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
+1
-1
@@ -172,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.
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -90,22 +90,24 @@ const variableKey = (item = {}) => {
|
||||
}"
|
||||
@click="onListItemSelection(index)"
|
||||
>
|
||||
<p
|
||||
class="max-w-full min-w-0 mb-0 overflow-hidden text-sm font-medium text-slate-900 dark:text-slate-100 group-hover:text-woot-500 dark:group-hover:text-woot-500 text-ellipsis whitespace-nowrap"
|
||||
:class="{
|
||||
'text-woot-500 dark:text-woot-500': index === selectedIndex,
|
||||
}"
|
||||
>
|
||||
{{ item.description }}
|
||||
</p>
|
||||
<p
|
||||
class="max-w-full min-w-0 mb-0 overflow-hidden text-xs text-slate-500 dark:text-slate-300 group-hover:text-woot-500 dark:group-hover:text-woot-500 text-ellipsis whitespace-nowrap"
|
||||
:class="{
|
||||
'text-woot-500 dark:text-woot-500': index === selectedIndex,
|
||||
}"
|
||||
>
|
||||
{{ variableKey(item) }}
|
||||
</p>
|
||||
<slot :item="item" :index="index" :selected="index === selectedIndex">
|
||||
<p
|
||||
class="max-w-full min-w-0 mb-0 overflow-hidden text-sm font-medium text-slate-900 dark:text-slate-100 group-hover:text-woot-500 dark:group-hover:text-woot-500 text-ellipsis whitespace-nowrap"
|
||||
:class="{
|
||||
'text-woot-500 dark:text-woot-500': index === selectedIndex,
|
||||
}"
|
||||
>
|
||||
{{ item.description }}
|
||||
</p>
|
||||
<p
|
||||
class="max-w-full min-w-0 mb-0 overflow-hidden text-xs text-slate-500 dark:text-slate-300 group-hover:text-woot-500 dark:group-hover:text-woot-500 text-ellipsis whitespace-nowrap"
|
||||
:class="{
|
||||
'text-woot-500 dark:text-woot-500': index === selectedIndex,
|
||||
}"
|
||||
>
|
||||
{{ variableKey(item) }}
|
||||
</p>
|
||||
</slot>
|
||||
</button>
|
||||
</woot-dropdown-item>
|
||||
</ul>
|
||||
|
||||
@@ -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,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,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);
|
||||
});
|
||||
});
|
||||
@@ -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,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,
|
||||
};
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
];
|
||||
@@ -5,6 +5,7 @@ export const CONVERSATION_EVENTS = Object.freeze({
|
||||
INSERTED_A_CANNED_RESPONSE: 'Inserted a canned response',
|
||||
TRANSLATE_A_MESSAGE: 'Translated a message',
|
||||
INSERTED_A_VARIABLE: 'Inserted a variable',
|
||||
INSERTED_AN_EMOJI: 'Inserted an emoji',
|
||||
USED_MENTIONS: 'Used mentions',
|
||||
SEARCH_CONVERSATION: 'Searched conversations',
|
||||
APPLY_FILTER: 'Applied filters in the conversation list',
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
@@ -3,6 +3,8 @@ import {
|
||||
MessageMarkdownTransformer,
|
||||
MessageMarkdownSerializer,
|
||||
} from '@chatwoot/prosemirror-schema';
|
||||
import { replaceVariablesInMessage } from '@chatwoot/utils';
|
||||
|
||||
import * as Sentry from '@sentry/browser';
|
||||
|
||||
/**
|
||||
@@ -281,3 +283,92 @@ export function setURLWithQueryAndSize(selectedImageNode, size, editorView) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Content Node Creation Helper Functions for
|
||||
* - mention
|
||||
* - canned response
|
||||
* - variable
|
||||
* - emoji
|
||||
*/
|
||||
|
||||
/**
|
||||
* Centralized node creation function that handles the creation of different types of nodes based on the specified type.
|
||||
* @param {Object} editorView - The editor view instance.
|
||||
* @param {string} nodeType - The type of node to create ('mention', 'cannedResponse', 'variable', 'emoji').
|
||||
* @param {Object|string} content - The content needed to create the node, which varies based on node type.
|
||||
* @returns {Object|null} - The created ProseMirror node or null if the type is not supported.
|
||||
*/
|
||||
const createNode = (editorView, nodeType, content) => {
|
||||
const { state } = editorView;
|
||||
switch (nodeType) {
|
||||
case 'mention':
|
||||
return state.schema.nodes.mention.create({
|
||||
userId: content.id,
|
||||
userFullName: content.name,
|
||||
});
|
||||
case 'cannedResponse':
|
||||
return new MessageMarkdownTransformer(messageSchema).parse(content);
|
||||
case 'variable':
|
||||
return state.schema.text(`{{${content}}}`);
|
||||
case 'emoji':
|
||||
return state.schema.text(content);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Object mapping types to their respective node creation functions.
|
||||
*/
|
||||
const nodeCreators = {
|
||||
mention: (editorView, content, from, to) => ({
|
||||
node: createNode(editorView, 'mention', content),
|
||||
from,
|
||||
to,
|
||||
}),
|
||||
cannedResponse: (editorView, content, from, to, variables) => {
|
||||
const updatedMessage = replaceVariablesInMessage({
|
||||
message: content,
|
||||
variables,
|
||||
});
|
||||
const node = createNode(editorView, 'cannedResponse', updatedMessage);
|
||||
return {
|
||||
node,
|
||||
from: node.textContent === updatedMessage ? from : from - 1,
|
||||
to,
|
||||
};
|
||||
},
|
||||
variable: (editorView, content, from, to) => ({
|
||||
node: createNode(editorView, 'variable', content),
|
||||
from,
|
||||
to,
|
||||
}),
|
||||
emoji: (editorView, content, from, to) => ({
|
||||
node: createNode(editorView, 'emoji', content),
|
||||
from,
|
||||
to,
|
||||
}),
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves a content node based on the specified type and content, using a functional approach to select the appropriate node creation function.
|
||||
* @param {Object} editorView - The editor view instance.
|
||||
* @param {string} type - The type of content node to create ('mention', 'cannedResponse', 'variable', 'emoji').
|
||||
* @param {string|Object} content - The content to be transformed into a node.
|
||||
* @param {Object} range - An object containing 'from' and 'to' properties indicating the range in the document where the node should be placed.
|
||||
* @param {Object} variables - Optional. Variables to replace in the content, used for 'cannedResponse' type.
|
||||
* @returns {Object} - An object containing the created node and the updated 'from' and 'to' positions.
|
||||
*/
|
||||
export const getContentNode = (
|
||||
editorView,
|
||||
type,
|
||||
content,
|
||||
{ from, to },
|
||||
variables
|
||||
) => {
|
||||
const creator = nodeCreators[type];
|
||||
return creator
|
||||
? creator(editorView, content, from, to, variables)
|
||||
: { node: null, from, to };
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ const FEATURE_HELP_URLS = {
|
||||
custom_attributes: 'https://chwt.app/hc/custom-attributes',
|
||||
dashboard_apps: 'https://chwt.app/hc/dashboard-apps',
|
||||
help_center: 'https://chwt.app/hc/help-center',
|
||||
inboxes: 'https://chwt.app/hc/inboxes',
|
||||
integrations: 'https://chwt.app/hc/integrations',
|
||||
labels: 'https://chwt.app/hc/labels',
|
||||
macros: 'https://chwt.app/hc/macros',
|
||||
@@ -16,6 +17,7 @@ const FEATURE_HELP_URLS = {
|
||||
reports: 'https://chwt.app/hc/reports',
|
||||
sla: 'https://chwt.app/hc/sla',
|
||||
team_management: 'https://chwt.app/hc/teams',
|
||||
webhook: 'https://chwt.app/hc/webhooks',
|
||||
};
|
||||
|
||||
export function getHelpUrlForFeature(featureName) {
|
||||
|
||||
+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,127 @@
|
||||
// Moved from editorHelper.spec.js to editorContentHelper.spec.js
|
||||
// the mock of chatwoot/prosemirror-schema is getting conflicted with other specs
|
||||
import { getContentNode } from '../editorHelper';
|
||||
import {
|
||||
MessageMarkdownTransformer,
|
||||
messageSchema,
|
||||
} from '@chatwoot/prosemirror-schema';
|
||||
import { replaceVariablesInMessage } from '@chatwoot/utils';
|
||||
|
||||
vi.mock('@chatwoot/prosemirror-schema', () => ({
|
||||
MessageMarkdownTransformer: vi.fn(),
|
||||
messageSchema: {},
|
||||
}));
|
||||
|
||||
vi.mock('@chatwoot/utils', () => ({
|
||||
replaceVariablesInMessage: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('getContentNode', () => {
|
||||
let editorView;
|
||||
|
||||
beforeEach(() => {
|
||||
editorView = {
|
||||
state: {
|
||||
schema: {
|
||||
nodes: {
|
||||
mention: {
|
||||
create: vi.fn(),
|
||||
},
|
||||
},
|
||||
text: vi.fn(),
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('getMentionNode', () => {
|
||||
it('should create a mention node', () => {
|
||||
const content = { id: 1, name: 'John Doe' };
|
||||
const from = 0;
|
||||
const to = 10;
|
||||
getContentNode(editorView, 'mention', content, {
|
||||
from,
|
||||
to,
|
||||
});
|
||||
|
||||
expect(editorView.state.schema.nodes.mention.create).toHaveBeenCalledWith(
|
||||
{
|
||||
userId: content.id,
|
||||
userFullName: content.name,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCannedResponseNode', () => {
|
||||
it('should create a canned response node', () => {
|
||||
const content = 'Hello {{name}}';
|
||||
const variables = { name: 'John' };
|
||||
const from = 0;
|
||||
const to = 10;
|
||||
const updatedMessage = 'Hello John';
|
||||
|
||||
replaceVariablesInMessage.mockReturnValue(updatedMessage);
|
||||
MessageMarkdownTransformer.mockImplementation(() => ({
|
||||
parse: vi.fn().mockReturnValue({ textContent: updatedMessage }),
|
||||
}));
|
||||
|
||||
const { node } = getContentNode(
|
||||
editorView,
|
||||
'cannedResponse',
|
||||
content,
|
||||
{ from, to },
|
||||
variables
|
||||
);
|
||||
|
||||
expect(replaceVariablesInMessage).toHaveBeenCalledWith({
|
||||
message: content,
|
||||
variables,
|
||||
});
|
||||
expect(MessageMarkdownTransformer).toHaveBeenCalledWith(messageSchema);
|
||||
expect(node.textContent).toBe(updatedMessage);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVariableNode', () => {
|
||||
it('should create a variable node', () => {
|
||||
const content = 'name';
|
||||
const from = 0;
|
||||
const to = 10;
|
||||
getContentNode(editorView, 'variable', content, {
|
||||
from,
|
||||
to,
|
||||
});
|
||||
|
||||
expect(editorView.state.schema.text).toHaveBeenCalledWith('{{name}}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEmojiNode', () => {
|
||||
it('should create an emoji node', () => {
|
||||
const content = '😊';
|
||||
const from = 0;
|
||||
const to = 2;
|
||||
getContentNode(editorView, 'emoji', content, {
|
||||
from,
|
||||
to,
|
||||
});
|
||||
|
||||
expect(editorView.state.schema.text).toHaveBeenCalledWith('😊');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getContentNode', () => {
|
||||
it('should return null for invalid type', () => {
|
||||
const content = 'invalid';
|
||||
const from = 0;
|
||||
const to = 10;
|
||||
const { node } = getContentNode(editorView, 'invalid', content, {
|
||||
from,
|
||||
to,
|
||||
});
|
||||
|
||||
expect(node).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
+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,52 +1,93 @@
|
||||
import { uploadFile } from '../uploadHelper';
|
||||
import axios from 'axios';
|
||||
import { uploadExternalImage, uploadFile } from '../uploadHelper';
|
||||
|
||||
global.axios = axios;
|
||||
vi.mock('axios');
|
||||
|
||||
describe('#Upload Helpers', () => {
|
||||
describe('Upload Helpers', () => {
|
||||
afterEach(() => {
|
||||
// Cleaning up the mock after each test
|
||||
axios.post.mockReset();
|
||||
});
|
||||
|
||||
it('should send a POST request with correct data', async () => {
|
||||
const mockFile = new File(['dummy content'], 'example.png', {
|
||||
type: 'image/png',
|
||||
describe('uploadFile', () => {
|
||||
it('should send a POST request with correct data', async () => {
|
||||
const mockFile = new File(['dummy content'], 'example.png', {
|
||||
type: 'image/png',
|
||||
});
|
||||
const mockResponse = {
|
||||
data: {
|
||||
file_url: 'https://example.com/fileUrl',
|
||||
blob_key: 'blobKey123',
|
||||
blob_id: 'blobId456',
|
||||
},
|
||||
};
|
||||
|
||||
axios.post.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await uploadFile(mockFile, '1602');
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
'/api/v1/accounts/1602/upload',
|
||||
expect.any(FormData),
|
||||
{ headers: { 'Content-Type': 'multipart/form-data' } }
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
fileUrl: 'https://example.com/fileUrl',
|
||||
blobKey: 'blobKey123',
|
||||
blobId: 'blobId456',
|
||||
});
|
||||
});
|
||||
const mockResponse = {
|
||||
data: {
|
||||
file_url: 'https://example.com/fileUrl',
|
||||
blob_key: 'blobKey123',
|
||||
blob_id: 'blobId456',
|
||||
},
|
||||
};
|
||||
|
||||
axios.post.mockResolvedValueOnce(mockResponse);
|
||||
it('should handle errors', async () => {
|
||||
const mockFile = new File(['dummy content'], 'example.png', {
|
||||
type: 'image/png',
|
||||
});
|
||||
const mockError = new Error('Failed to upload');
|
||||
|
||||
const result = await uploadFile(mockFile, '1602');
|
||||
axios.post.mockRejectedValueOnce(mockError);
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
'/api/v1/accounts/1602/upload',
|
||||
expect.any(FormData),
|
||||
{ headers: { 'Content-Type': 'multipart/form-data' } }
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
fileUrl: 'https://example.com/fileUrl',
|
||||
blobKey: 'blobKey123',
|
||||
blobId: 'blobId456',
|
||||
await expect(uploadFile(mockFile)).rejects.toThrow('Failed to upload');
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle errors', async () => {
|
||||
const mockFile = new File(['dummy content'], 'example.png', {
|
||||
type: 'image/png',
|
||||
describe('uploadExternalImage', () => {
|
||||
it('should send a POST request with correct data', async () => {
|
||||
const mockUrl = 'https://example.com/image.jpg';
|
||||
const mockResponse = {
|
||||
data: {
|
||||
file_url: 'https://example.com/fileUrl',
|
||||
blob_key: 'blobKey123',
|
||||
blob_id: 'blobId456',
|
||||
},
|
||||
};
|
||||
|
||||
axios.post.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await uploadExternalImage(mockUrl, '1602');
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
'/api/v1/accounts/1602/upload',
|
||||
{ external_url: mockUrl },
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
fileUrl: 'https://example.com/fileUrl',
|
||||
blobKey: 'blobKey123',
|
||||
blobId: 'blobId456',
|
||||
});
|
||||
});
|
||||
const mockError = new Error('Failed to upload');
|
||||
|
||||
axios.post.mockRejectedValueOnce(mockError);
|
||||
it('should handle errors', async () => {
|
||||
const mockUrl = 'https://example.com/image.jpg';
|
||||
const mockError = new Error('Failed to upload');
|
||||
|
||||
await expect(uploadFile(mockFile)).rejects.toThrow('Failed to upload');
|
||||
axios.post.mockRejectedValueOnce(mockError);
|
||||
|
||||
await expect(uploadExternalImage(mockUrl)).rejects.toThrow(
|
||||
'Failed to upload'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,26 +19,47 @@ const HEADERS = {
|
||||
* The function uses FormData to wrap the file and axios to send the request.
|
||||
*
|
||||
* @param {File} file - The file to be uploaded. It should be a File object (typically coming from a file input element).
|
||||
* @param {string} accountId - The account ID.
|
||||
* @returns {Promise} A promise that resolves with the server's response when the upload is successful, or rejects if there's an error.
|
||||
*/
|
||||
export async function uploadFile(file, accountId) {
|
||||
// Create a new FormData instance.
|
||||
let formData = new FormData();
|
||||
|
||||
if (!accountId) {
|
||||
accountId = window.location.pathname.split('/')[3];
|
||||
}
|
||||
|
||||
// Append the file to the FormData instance under the key 'attachment'.
|
||||
let formData = new FormData();
|
||||
formData.append('attachment', file);
|
||||
|
||||
// Use axios to send a POST request to the upload endpoint.
|
||||
const { data } = await axios.post(
|
||||
`/api/${API_VERSION}/accounts/${accountId}/upload`,
|
||||
formData,
|
||||
{
|
||||
headers: HEADERS,
|
||||
}
|
||||
{ headers: HEADERS }
|
||||
);
|
||||
|
||||
return {
|
||||
fileUrl: data.file_url,
|
||||
blobKey: data.blob_key,
|
||||
blobId: data.blob_id,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads an image from an external URL.
|
||||
*
|
||||
* @param {string} url - The external URL of the image.
|
||||
* @param {string} accountId - The account ID.
|
||||
* @returns {Promise} A promise that resolves with the server's response.
|
||||
*/
|
||||
export async function uploadExternalImage(url, accountId) {
|
||||
if (!accountId) {
|
||||
accountId = window.location.pathname.split('/')[3];
|
||||
}
|
||||
|
||||
const { data } = await axios.post(
|
||||
`/api/${API_VERSION}/accounts/${accountId}/upload`,
|
||||
{ external_url: url },
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -55,6 +55,9 @@
|
||||
"CREATED_AT": "Created at",
|
||||
"LAST_ACTIVITY": "Last activity"
|
||||
},
|
||||
"ERRORS": {
|
||||
"VALUE_REQUIRED": "Value is required"
|
||||
},
|
||||
"GROUPS": {
|
||||
"STANDARD_FILTERS": "Standard filters",
|
||||
"ADDITIONAL_FILTERS": "Additional filters",
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"HEADER": "Custom Attributes",
|
||||
"HEADER_BTN_TXT": "Add Custom Attribute",
|
||||
"LOADING": "Fetching custom attributes",
|
||||
"SIDEBAR_TXT": "<p><b>Custom Attributes</b> <p>A custom attribute tracks facts about your contacts/conversation — like the subscription plan, or when they ordered the first item etc. <br /><br />For creating a Custom Attribute, just click on the <b>Add Custom Attribute.</b> You can also edit or delete an existing Custom Attribute by clicking on the Edit or Delete button.</p>",
|
||||
"DESCRIPTION": "A custom attribute tracks additional details about your contacts or conversations—such as the subscription plan or the date of their first purchase. You can add different types of custom attributes, such as text, lists, or numbers, to capture the specific information you need.",
|
||||
"LEARN_MORE": "Learn more about custom attributes",
|
||||
"ADD": {
|
||||
"TITLE": "Add Custom Attribute",
|
||||
"SUBMIT": "Create",
|
||||
@@ -91,7 +92,12 @@
|
||||
"CONTACT": "Contact"
|
||||
},
|
||||
"LIST": {
|
||||
"TABLE_HEADER": ["Name", "Description", "Type", "Key"],
|
||||
"TABLE_HEADER": [
|
||||
"Name",
|
||||
"Description",
|
||||
"Type",
|
||||
"Key"
|
||||
],
|
||||
"BUTTONS": {
|
||||
"EDIT": "Edit",
|
||||
"DELETE": "Delete"
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
"is_lesser_than": "Is lesser than",
|
||||
"days_before": "Is x days before"
|
||||
},
|
||||
"ERRORS": {
|
||||
"VALUE_REQUIRED": "Value is required"
|
||||
},
|
||||
"ATTRIBUTES": {
|
||||
"NAME": "Name",
|
||||
"EMAIL": "Email",
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"UPLOADING": "Uploading...",
|
||||
"SUCCESS": "Image uploaded successfully",
|
||||
"ERROR": "Error while uploading image",
|
||||
"UN_AUTHORIZED_ERROR": "You are not authorized to upload images",
|
||||
"ERROR_FILE_SIZE": "Image size should be less than {size}MB",
|
||||
"ERROR_FILE_FORMAT": "Image format should be jpg, jpeg or png",
|
||||
"ERROR_FILE_DIMENSIONS": "Image dimensions should be less than 2000 x 2000"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"INBOX_MGMT": {
|
||||
"HEADER": "Inboxes",
|
||||
"SIDEBAR_TXT": "<p><b>Inbox</b></p> <p> When you connect a website or a facebook Page to Chatwoot, it is called an <b>Inbox</b>. You can have unlimited inboxes in your Chatwoot account. </p><p> Click on <b>Add Inbox</b> to connect a website or a Facebook Page. </p><p> In the Dashboard, you can see all the conversations from all your inboxes in a single place and respond to them under the `Conversations` tab. </p><p> You can also see conversations specific to an inbox by clicking on the inbox name on the left pane of the dashboard. </p>",
|
||||
"DESCRIPTION": "A channel is the mode of communication your customer chooses to interact with you. An inbox is where you manage interactions for a specific channel. It can include communications from various sources such as email, live chat, and social media.",
|
||||
"LEARN_MORE": "Learn more about inboxes",
|
||||
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
|
||||
"CLICK_TO_RECONNECT": "Click here to reconnect.",
|
||||
"LIST": {
|
||||
@@ -745,6 +746,18 @@
|
||||
"MICROSOFT": "Microsoft",
|
||||
"GOOGLE": "Google",
|
||||
"OTHER_PROVIDERS": "Other Providers"
|
||||
},
|
||||
"CHANNELS": {
|
||||
"MESSENGER": "Messenger",
|
||||
"WEB_WIDGET": "Website",
|
||||
"TWITTER_PROFILE": "Twitter",
|
||||
"TWILIO_SMS": "Twilio SMS",
|
||||
"WHATSAPP": "WhatsApp",
|
||||
"SMS": "SMS",
|
||||
"EMAIL": "Email",
|
||||
"TELEGRAM": "Telegram",
|
||||
"LINE": "Line",
|
||||
"API": "API Channel"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
},
|
||||
"WEBHOOK": {
|
||||
"SUBSCRIBED_EVENTS": "Subscribed Events",
|
||||
"LEARN_MORE": "Learn more about webhooks",
|
||||
"FORM": {
|
||||
"CANCEL": "Cancel",
|
||||
"DESC": "Webhook events provide you the realtime information about what's happening in your Chatwoot account. Please enter a valid URL to configure a callback.",
|
||||
|
||||
@@ -1,314 +0,0 @@
|
||||
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||
import countries from 'shared/constants/countries';
|
||||
import { validateAutomation } from 'dashboard/helper/validations';
|
||||
|
||||
import {
|
||||
generateCustomAttributeTypes,
|
||||
getActionOptions,
|
||||
getConditionOptions,
|
||||
getCustomAttributeInputType,
|
||||
getOperatorTypes,
|
||||
isACustomAttribute,
|
||||
getFileName,
|
||||
getDefaultConditions,
|
||||
getDefaultActions,
|
||||
filterCustomAttributes,
|
||||
generateAutomationPayload,
|
||||
getStandardAttributeInputType,
|
||||
isCustomAttribute,
|
||||
generateCustomAttributes,
|
||||
} from 'dashboard/helper/automationHelper';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapGetters({
|
||||
agents: 'agents/getAgents',
|
||||
campaigns: 'campaigns/getAllCampaigns',
|
||||
contacts: 'contacts/getContacts',
|
||||
inboxes: 'inboxes/getInboxes',
|
||||
labels: 'labels/getLabels',
|
||||
teams: 'teams/getTeams',
|
||||
slaPolicies: 'sla/getSLA',
|
||||
}),
|
||||
booleanFilterOptions() {
|
||||
return [
|
||||
{
|
||||
id: true,
|
||||
name: this.$t('FILTER.ATTRIBUTE_LABELS.TRUE'),
|
||||
},
|
||||
{
|
||||
id: false,
|
||||
name: this.$t('FILTER.ATTRIBUTE_LABELS.FALSE'),
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
statusFilterOptions() {
|
||||
const statusFilters = this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS');
|
||||
return [
|
||||
...Object.keys(statusFilters).map(status => {
|
||||
return {
|
||||
id: status,
|
||||
name: statusFilters[status].TEXT,
|
||||
};
|
||||
}),
|
||||
{
|
||||
id: 'all',
|
||||
name: this.$t('CHAT_LIST.FILTER_ALL'),
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getFileName,
|
||||
onEventChange() {
|
||||
this.automation.conditions = getDefaultConditions(
|
||||
this.automation.event_name
|
||||
);
|
||||
this.automation.actions = getDefaultActions();
|
||||
},
|
||||
getAttributes(key) {
|
||||
return this.automationTypes[key].conditions;
|
||||
},
|
||||
getInputType(key) {
|
||||
const customAttribute = isACustomAttribute(this.allCustomAttributes, key);
|
||||
if (customAttribute) {
|
||||
return getCustomAttributeInputType(
|
||||
customAttribute.attribute_display_type
|
||||
);
|
||||
}
|
||||
const type = this.getAutomationType(key);
|
||||
return type.inputType;
|
||||
},
|
||||
getOperators(key) {
|
||||
if (this.mode === 'edit') {
|
||||
const customAttribute = isACustomAttribute(
|
||||
this.allCustomAttributes,
|
||||
key
|
||||
);
|
||||
if (customAttribute) {
|
||||
return getOperatorTypes(customAttribute.attribute_display_type);
|
||||
}
|
||||
}
|
||||
const type = this.getAutomationType(key);
|
||||
return type.filterOperators;
|
||||
},
|
||||
getAutomationType(key) {
|
||||
return this.automationTypes[this.automation.event_name].conditions.find(
|
||||
condition => condition.key === key
|
||||
);
|
||||
},
|
||||
getCustomAttributeType(key) {
|
||||
const type = this.automationTypes[
|
||||
this.automation.event_name
|
||||
].conditions.find(i => i.key === key).customAttributeType;
|
||||
return type;
|
||||
},
|
||||
getConditionDropdownValues(type) {
|
||||
const {
|
||||
agents,
|
||||
allCustomAttributes: customAttributes,
|
||||
booleanFilterOptions,
|
||||
campaigns,
|
||||
contacts,
|
||||
inboxes,
|
||||
statusFilterOptions,
|
||||
teams,
|
||||
} = this;
|
||||
return getConditionOptions({
|
||||
agents,
|
||||
booleanFilterOptions,
|
||||
campaigns,
|
||||
contacts,
|
||||
customAttributes,
|
||||
inboxes,
|
||||
statusFilterOptions,
|
||||
teams,
|
||||
languages,
|
||||
countries,
|
||||
type,
|
||||
});
|
||||
},
|
||||
appendNewCondition() {
|
||||
this.automation.conditions.push(
|
||||
...getDefaultConditions(this.automation.event_name)
|
||||
);
|
||||
},
|
||||
appendNewAction() {
|
||||
this.automation.actions.push(...getDefaultActions());
|
||||
},
|
||||
removeFilter(index) {
|
||||
if (this.automation.conditions.length <= 1) {
|
||||
useAlert(this.$t('AUTOMATION.CONDITION.DELETE_MESSAGE'));
|
||||
} else {
|
||||
this.automation.conditions.splice(index, 1);
|
||||
}
|
||||
},
|
||||
removeAction(index) {
|
||||
if (this.automation.actions.length <= 1) {
|
||||
useAlert(this.$t('AUTOMATION.ACTION.DELETE_MESSAGE'));
|
||||
} else {
|
||||
this.automation.actions.splice(index, 1);
|
||||
}
|
||||
},
|
||||
submitAutomation() {
|
||||
// we assign it to this.errors so that it can be accessed in the template
|
||||
// it is supposed to be declared in the data function
|
||||
this.errors = validateAutomation(this.automation);
|
||||
if (Object.keys(this.errors).length === 0) {
|
||||
const automation = generateAutomationPayload(this.automation);
|
||||
this.$emit('saveAutomation', automation, this.mode);
|
||||
}
|
||||
},
|
||||
resetFilter(index, currentCondition) {
|
||||
this.automation.conditions[index].filter_operator = this.automationTypes[
|
||||
this.automation.event_name
|
||||
].conditions.find(
|
||||
condition => condition.key === currentCondition.attribute_key
|
||||
).filterOperators[0].value;
|
||||
this.automation.conditions[index].values = '';
|
||||
},
|
||||
showUserInput(type) {
|
||||
return !(type === 'is_present' || type === 'is_not_present');
|
||||
},
|
||||
showActionInput(action) {
|
||||
if (action === 'send_email_to_team' || action === 'send_message')
|
||||
return false;
|
||||
const type = this.automationActionTypes.find(
|
||||
i => i.key === action
|
||||
).inputType;
|
||||
return !!type;
|
||||
},
|
||||
resetAction(index) {
|
||||
this.automation.actions[index].action_params = [];
|
||||
},
|
||||
manifestConditions(automation) {
|
||||
const customAttributes = filterCustomAttributes(this.allCustomAttributes);
|
||||
const conditions = automation.conditions.map(condition => {
|
||||
const customAttr = isCustomAttribute(
|
||||
customAttributes,
|
||||
condition.attribute_key
|
||||
);
|
||||
let inputType = 'plain_text';
|
||||
if (customAttr) {
|
||||
inputType = getCustomAttributeInputType(customAttr.type);
|
||||
} else {
|
||||
inputType = getStandardAttributeInputType(
|
||||
this.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: [
|
||||
...this.getConditionDropdownValues(condition.attribute_key),
|
||||
].filter(item => [...condition.values].includes(item.id)),
|
||||
};
|
||||
});
|
||||
return conditions;
|
||||
},
|
||||
generateActionsArray(action) {
|
||||
const params = action.action_params;
|
||||
let actionParams = [];
|
||||
const inputType = this.automationActionTypes.find(
|
||||
item => item.key === action.action_name
|
||||
).inputType;
|
||||
if (inputType === 'multi_select' || inputType === 'search_select') {
|
||||
actionParams = [
|
||||
...this.getActionDropdownValues(action.action_name),
|
||||
].filter(item => [...params].includes(item.id));
|
||||
} else if (inputType === 'team_message') {
|
||||
actionParams = {
|
||||
team_ids: [
|
||||
...this.getActionDropdownValues(action.action_name),
|
||||
].filter(item => [...params[0].team_ids].includes(item.id)),
|
||||
message: params[0].message,
|
||||
};
|
||||
} else actionParams = [...params];
|
||||
return actionParams;
|
||||
},
|
||||
manifestActions(automation) {
|
||||
let actionParams = [];
|
||||
const actions = automation.actions.map(action => {
|
||||
if (action.action_params.length) {
|
||||
actionParams = this.generateActionsArray(action);
|
||||
}
|
||||
return {
|
||||
...action,
|
||||
action_params: actionParams,
|
||||
};
|
||||
});
|
||||
return actions;
|
||||
},
|
||||
formatAutomation(automation) {
|
||||
this.automation = {
|
||||
...automation,
|
||||
conditions: this.manifestConditions(automation),
|
||||
actions: this.manifestActions(automation),
|
||||
};
|
||||
},
|
||||
getActionDropdownValues(type) {
|
||||
const { agents, labels, teams, slaPolicies } = this;
|
||||
return getActionOptions({
|
||||
agents,
|
||||
labels,
|
||||
teams,
|
||||
slaPolicies,
|
||||
languages,
|
||||
type,
|
||||
});
|
||||
},
|
||||
manifestCustomAttributes() {
|
||||
const conversationCustomAttributesRaw = this.$store.getters[
|
||||
'attributes/getAttributesByModel'
|
||||
]('conversation_attribute');
|
||||
|
||||
const contactCustomAttributesRaw =
|
||||
this.$store.getters['attributes/getAttributesByModel'](
|
||||
'contact_attribute'
|
||||
);
|
||||
const conversationCustomAttributeTypes = generateCustomAttributeTypes(
|
||||
conversationCustomAttributesRaw,
|
||||
'conversation_attribute'
|
||||
);
|
||||
const contactCustomAttributeTypes = generateCustomAttributeTypes(
|
||||
contactCustomAttributesRaw,
|
||||
'contact_attribute'
|
||||
);
|
||||
let manifestedCustomAttributes = generateCustomAttributes(
|
||||
conversationCustomAttributeTypes,
|
||||
contactCustomAttributeTypes,
|
||||
this.$t('AUTOMATION.CONDITION.CONVERSATION_CUSTOM_ATTR_LABEL'),
|
||||
this.$t('AUTOMATION.CONDITION.CONTACT_CUSTOM_ATTR_LABEL')
|
||||
);
|
||||
this.automationTypes.message_created.conditions.push(
|
||||
...manifestedCustomAttributes
|
||||
);
|
||||
this.automationTypes.conversation_created.conditions.push(
|
||||
...manifestedCustomAttributes
|
||||
);
|
||||
this.automationTypes.conversation_updated.conditions.push(
|
||||
...manifestedCustomAttributes
|
||||
);
|
||||
this.automationTypes.conversation_opened.conditions.push(
|
||||
...manifestedCustomAttributes
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import { formatTime } from '@chatwoot/utils';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
accountSummaryKey: {
|
||||
type: String,
|
||||
default: 'getAccountSummary',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountReport: 'getAccountReports',
|
||||
}),
|
||||
accountSummary() {
|
||||
return this.$store.getters[this.accountSummaryKey];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
calculateTrend(key) {
|
||||
if (!this.accountSummary.previous[key]) return 0;
|
||||
const diff = this.accountSummary[key] - this.accountSummary.previous[key];
|
||||
return Math.round((diff / this.accountSummary.previous[key]) * 100);
|
||||
},
|
||||
displayMetric(key) {
|
||||
if (this.isAverageMetricType(key)) {
|
||||
return formatTime(this.accountSummary[key]);
|
||||
}
|
||||
return Number(this.accountSummary[key] || '').toLocaleString();
|
||||
},
|
||||
displayInfoText(key) {
|
||||
if (this.metrics[this.currentSelection].KEY !== key) {
|
||||
return '';
|
||||
}
|
||||
if (this.isAverageMetricType(key)) {
|
||||
const total = this.accountReport.data
|
||||
.map(item => item.count)
|
||||
.reduce((prev, curr) => prev + curr, 0);
|
||||
return `${this.metrics[this.currentSelection].INFO_TEXT} ${total}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
isAverageMetricType(key) {
|
||||
return [
|
||||
'avg_first_response_time',
|
||||
'avg_resolution_time',
|
||||
'reply_time',
|
||||
].includes(key);
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,136 +0,0 @@
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import reportMixin from '../reportMixin';
|
||||
import reportFixtures from './reportMixinFixtures';
|
||||
import Vuex from 'vuex';
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
describe('reportMixin', () => {
|
||||
let getters;
|
||||
let store;
|
||||
beforeEach(() => {
|
||||
getters = {
|
||||
getAccountSummary: () => reportFixtures.summary,
|
||||
getBotSummary: () => reportFixtures.botSummary,
|
||||
getAccountReports: () => reportFixtures.report,
|
||||
};
|
||||
store = new Vuex.Store({ getters });
|
||||
});
|
||||
|
||||
it('display the metric for account', async () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [reportMixin],
|
||||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
await wrapper.setProps({
|
||||
accountSummaryKey: 'getAccountSummary',
|
||||
});
|
||||
expect(wrapper.vm.displayMetric('conversations_count')).toEqual('5,000');
|
||||
expect(wrapper.vm.displayMetric('avg_first_response_time')).toEqual(
|
||||
'3 Min 18 Sec'
|
||||
);
|
||||
});
|
||||
|
||||
it('display the metric for bot', async () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [reportMixin],
|
||||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
await wrapper.setProps({
|
||||
accountSummaryKey: 'getBotSummary',
|
||||
});
|
||||
expect(wrapper.vm.displayMetric('bot_resolutions_count')).toEqual('10');
|
||||
expect(wrapper.vm.displayMetric('bot_handoffs_count')).toEqual('20');
|
||||
});
|
||||
|
||||
it('display the metric', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [reportMixin],
|
||||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
expect(wrapper.vm.displayMetric('conversations_count')).toEqual('5,000');
|
||||
expect(wrapper.vm.displayMetric('avg_first_response_time')).toEqual(
|
||||
'3 Min 18 Sec'
|
||||
);
|
||||
});
|
||||
|
||||
it('calculate the trend', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [reportMixin],
|
||||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
expect(wrapper.vm.calculateTrend('conversations_count')).toEqual(124900);
|
||||
expect(wrapper.vm.calculateTrend('resolutions_count')).toEqual(0);
|
||||
});
|
||||
|
||||
it('display info text', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [reportMixin],
|
||||
data() {
|
||||
return {
|
||||
currentSelection: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
metrics() {
|
||||
return [
|
||||
{
|
||||
DESC: '( Avg )',
|
||||
INFO_TEXT: 'Total number of conversations used for computation:',
|
||||
KEY: 'avg_first_response_time',
|
||||
NAME: 'First Response Time',
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
expect(wrapper.vm.displayInfoText('avg_first_response_time')).toEqual(
|
||||
'Total number of conversations used for computation: 4'
|
||||
);
|
||||
});
|
||||
|
||||
it('do not display info text', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [reportMixin],
|
||||
data() {
|
||||
return {
|
||||
currentSelection: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
metrics() {
|
||||
return [
|
||||
{
|
||||
DESC: '( Total )',
|
||||
INFO_TEXT: '',
|
||||
KEY: 'conversation_count',
|
||||
NAME: 'Conversations',
|
||||
},
|
||||
{
|
||||
DESC: '( Avg )',
|
||||
INFO_TEXT: 'Total number of conversations used for computation:',
|
||||
KEY: 'avg_first_response_time',
|
||||
NAME: 'First Response Time',
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
expect(wrapper.vm.displayInfoText('conversation_count')).toEqual('');
|
||||
expect(wrapper.vm.displayInfoText('incoming_messages_count')).toEqual('');
|
||||
});
|
||||
});
|
||||
@@ -1,37 +0,0 @@
|
||||
export default {
|
||||
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,
|
||||
},
|
||||
botSummary: {
|
||||
bot_resolutions_count: 10,
|
||||
bot_handoffs_count: 20,
|
||||
previous: {
|
||||
bot_resolutions_count: 8,
|
||||
bot_handoffs_count: 5,
|
||||
},
|
||||
},
|
||||
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 },
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { mapGetters } from 'vuex';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import AddCannedModal from 'dashboard/routes/dashboard/settings/canned/AddCanned.vue';
|
||||
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
||||
import { conversationUrl, frontendURL } from '../../../helper/URLHelper';
|
||||
@@ -18,7 +18,6 @@ export default {
|
||||
TranslateModal,
|
||||
MenuItem,
|
||||
},
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
message: {
|
||||
type: Object,
|
||||
@@ -37,6 +36,12 @@ export default {
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { getPlainText } = useMessageFormatter();
|
||||
return {
|
||||
getPlainText,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isCannedResponseModalOpen: false,
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
<script>
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
},
|
||||
|
||||
mixins: [messageFormatterMixin],
|
||||
|
||||
props: {
|
||||
id: {
|
||||
type: Number,
|
||||
@@ -28,6 +25,12 @@ export default {
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
return {
|
||||
formatMessage,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showDeleteModal: false,
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<script>
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import ReadMore from './ReadMore.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ReadMore,
|
||||
},
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
author: {
|
||||
type: String,
|
||||
@@ -21,6 +20,13 @@ export default {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { formatMessage, highlightContent } = useMessageFormatter();
|
||||
return {
|
||||
formatMessage,
|
||||
highlightContent,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isOverflowing: false,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { useEmitter } from 'dashboard/composables/emitter';
|
||||
import { getUnixTime } from 'date-fns';
|
||||
import { findSnoozeTime } from 'dashboard/helper/snoozeHelpers';
|
||||
import { CMD_SNOOZE_CONVERSATION } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
|
||||
import { CMD_SNOOZE_CONVERSATION } from 'dashboard/helper/commandbar/events';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import CustomSnoozeModal from 'dashboard/components/CustomSnoozeModal.vue';
|
||||
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import {
|
||||
ICON_APPEARANCE,
|
||||
ICON_LIGHT_MODE,
|
||||
ICON_DARK_MODE,
|
||||
ICON_SYSTEM_MODE,
|
||||
} from './CommandBarIcons';
|
||||
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||
import { setColorTheme } from 'dashboard/helper/themeHelper.js';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
themeOptions() {
|
||||
return [
|
||||
{
|
||||
key: 'light',
|
||||
label: this.$t('COMMAND_BAR.COMMANDS.LIGHT_MODE'),
|
||||
icon: ICON_LIGHT_MODE,
|
||||
},
|
||||
{
|
||||
key: 'dark',
|
||||
label: this.$t('COMMAND_BAR.COMMANDS.DARK_MODE'),
|
||||
icon: ICON_DARK_MODE,
|
||||
},
|
||||
|
||||
{
|
||||
key: 'auto',
|
||||
label: this.$t('COMMAND_BAR.COMMANDS.SYSTEM_MODE'),
|
||||
icon: ICON_SYSTEM_MODE,
|
||||
},
|
||||
];
|
||||
},
|
||||
goToAppearanceHotKeys() {
|
||||
const options = this.themeOptions.map(theme => ({
|
||||
id: theme.key,
|
||||
title: theme.label,
|
||||
parent: 'appearance_settings',
|
||||
section: this.$t('COMMAND_BAR.SECTIONS.APPEARANCE'),
|
||||
icon: theme.icon,
|
||||
handler: () => {
|
||||
this.setAppearance(theme.key);
|
||||
},
|
||||
}));
|
||||
return [
|
||||
{
|
||||
id: 'appearance_settings',
|
||||
title: this.$t('COMMAND_BAR.COMMANDS.CHANGE_APPEARANCE'),
|
||||
section: this.$t('COMMAND_BAR.SECTIONS.APPEARANCE'),
|
||||
icon: ICON_APPEARANCE,
|
||||
children: options.map(option => option.id),
|
||||
},
|
||||
...options,
|
||||
];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setAppearance(theme) {
|
||||
LocalStorage.set(LOCAL_STORAGE_KEYS.COLOR_SCHEME, theme);
|
||||
const isOSOnDarkMode = window.matchMedia(
|
||||
'(prefers-color-scheme: dark)'
|
||||
).matches;
|
||||
setColorTheme(isOSOnDarkMode);
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,86 +0,0 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
|
||||
import {
|
||||
CMD_BULK_ACTION_SNOOZE_CONVERSATION,
|
||||
CMD_BULK_ACTION_REOPEN_CONVERSATION,
|
||||
CMD_BULK_ACTION_RESOLVE_CONVERSATION,
|
||||
} from './commandBarBusEvents';
|
||||
import {
|
||||
ICON_SNOOZE_CONVERSATION,
|
||||
ICON_REOPEN_CONVERSATION,
|
||||
ICON_RESOLVE_CONVERSATION,
|
||||
} from './CommandBarIcons';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
import { createSnoozeHandlers } from './commandBarActions';
|
||||
|
||||
const SNOOZE_OPTIONS = wootConstants.SNOOZE_OPTIONS;
|
||||
|
||||
export 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'
|
||||
),
|
||||
];
|
||||
|
||||
export 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: () => emitter.emit(CMD_BULK_ACTION_REOPEN_CONVERSATION),
|
||||
},
|
||||
];
|
||||
|
||||
export 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: () => emitter.emit(CMD_BULK_ACTION_RESOLVE_CONVERSATION),
|
||||
},
|
||||
];
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapGetters({
|
||||
selectedConversations: 'bulkActions/getSelectedConversationIds',
|
||||
}),
|
||||
bulkActionsHotKeys() {
|
||||
let actions = [];
|
||||
if (this.selectedConversations.length > 0) {
|
||||
actions = [
|
||||
...SNOOZE_CONVERSATION_BULK_ACTIONS,
|
||||
...RESOLVED_CONVERSATION_BULK_ACTIONS,
|
||||
...OPEN_CONVERSATION_BULK_ACTIONS,
|
||||
];
|
||||
}
|
||||
return this.prepareActions(actions);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
selectedConversations() {
|
||||
this.setCommandbarData();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
prepareActions(actions) {
|
||||
return actions.map(action => ({
|
||||
...action,
|
||||
title: this.$t(action.title),
|
||||
section: this.$t(action.section),
|
||||
}));
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,118 +1,85 @@
|
||||
<script>
|
||||
<script setup>
|
||||
import '@chatwoot/ninja-keys';
|
||||
import { useConversationLabels } from 'dashboard/composables/useConversationLabels';
|
||||
import { useAI } from 'dashboard/composables/useAI';
|
||||
import { useAgentsList } from 'dashboard/composables/useAgentsList';
|
||||
import { ref, computed, watchEffect, onMounted } from 'vue';
|
||||
import { useStore } from 'dashboard/composables/store';
|
||||
import { useTrack } from 'dashboard/composables';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { useAppearanceHotKeys } from 'dashboard/composables/commands/useAppearanceHotKeys';
|
||||
import { useInboxHotKeys } from 'dashboard/composables/commands/useInboxHotKeys';
|
||||
import { useGoToCommandHotKeys } from 'dashboard/composables/commands/useGoToCommandHotKeys';
|
||||
import { useBulkActionsHotKeys } from 'dashboard/composables/commands/useBulkActionsHotKeys';
|
||||
import { useConversationHotKeys } from 'dashboard/composables/commands/useConversationHotKeys';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import conversationHotKeysMixin from './conversationHotKeys';
|
||||
import bulkActionsHotKeysMixin from './bulkActionsHotKeys';
|
||||
import inboxHotKeysMixin from './inboxHotKeys';
|
||||
import goToCommandHotKeys from './goToCommandHotKeys';
|
||||
import appearanceHotKeys from './appearanceHotKeys';
|
||||
import { GENERAL_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||
import { GENERAL_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
|
||||
export default {
|
||||
mixins: [
|
||||
conversationHotKeysMixin,
|
||||
bulkActionsHotKeysMixin,
|
||||
inboxHotKeysMixin,
|
||||
appearanceHotKeys,
|
||||
goToCommandHotKeys,
|
||||
],
|
||||
setup() {
|
||||
// used in conversationHotKeysMixin
|
||||
const {
|
||||
activeLabels,
|
||||
inactiveLabels,
|
||||
addLabelToConversation,
|
||||
removeLabelFromConversation,
|
||||
} = useConversationLabels();
|
||||
const store = useStore();
|
||||
const track = useTrack();
|
||||
const { t } = useI18n();
|
||||
|
||||
const { isAIIntegrationEnabled } = useAI();
|
||||
const { agentsList, assignableAgents } = useAgentsList();
|
||||
const ninjakeys = ref(null);
|
||||
|
||||
return {
|
||||
agentsList,
|
||||
assignableAgents,
|
||||
activeLabels,
|
||||
inactiveLabels,
|
||||
addLabelToConversation,
|
||||
removeLabelFromConversation,
|
||||
isAIIntegrationEnabled,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// Added selectedSnoozeType to track the selected snooze type
|
||||
// So if the selected snooze type is "custom snooze" then we set selectedSnoozeType with the CMD action id
|
||||
// So that we can track the selected snooze type and when we close the command bar
|
||||
selectedSnoozeType: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
placeholder() {
|
||||
return this.$t('COMMAND_BAR.SEARCH_PLACEHOLDER');
|
||||
},
|
||||
accountId() {
|
||||
return this.$store.getters.getCurrentAccountId;
|
||||
},
|
||||
routeName() {
|
||||
return this.$route.name;
|
||||
},
|
||||
hotKeys() {
|
||||
return [
|
||||
...this.inboxHotKeys,
|
||||
...this.conversationHotKeys,
|
||||
...this.bulkActionsHotKeys,
|
||||
...this.goToCommandHotKeys,
|
||||
...this.goToAppearanceHotKeys,
|
||||
];
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
routeName() {
|
||||
this.setCommandbarData();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setCommandbarData();
|
||||
},
|
||||
methods: {
|
||||
setCommandbarData() {
|
||||
this.$refs.ninjakeys.data = this.hotKeys;
|
||||
},
|
||||
onSelected(item) {
|
||||
const {
|
||||
detail: {
|
||||
action: { title = null, section = null, id = null } = {},
|
||||
} = {},
|
||||
} = item;
|
||||
// Added this condition to prevent setting the selectedSnoozeType to null
|
||||
// When we select the "custom snooze" (CMD bar will close and the custom snooze modal will open)
|
||||
if (id === wootConstants.SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME) {
|
||||
this.selectedSnoozeType =
|
||||
wootConstants.SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME;
|
||||
} else {
|
||||
this.selectedSnoozeType = null;
|
||||
}
|
||||
this.$track(GENERAL_EVENTS.COMMAND_BAR, {
|
||||
section,
|
||||
action: title,
|
||||
});
|
||||
this.setCommandbarData();
|
||||
},
|
||||
onClosed() {
|
||||
// If the selectedSnoozeType is not "SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME (custom snooze)" then we set the context menu chat id to null
|
||||
// Else we do nothing and its handled in the ChatList.vue hideCustomSnoozeModal() method
|
||||
if (
|
||||
this.selectedSnoozeType !==
|
||||
wootConstants.SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME
|
||||
) {
|
||||
this.$store.dispatch('setContextMenuChatId', null);
|
||||
}
|
||||
},
|
||||
},
|
||||
// Added selectedSnoozeType to track the selected snooze type
|
||||
// So if the selected snooze type is "custom snooze" then we set selectedSnoozeType with the CMD action id
|
||||
// So that we can track the selected snooze type and when we close the command bar
|
||||
const selectedSnoozeType = ref(null);
|
||||
|
||||
const { goToAppearanceHotKeys } = useAppearanceHotKeys();
|
||||
const { inboxHotKeys } = useInboxHotKeys();
|
||||
const { goToCommandHotKeys } = useGoToCommandHotKeys();
|
||||
const { bulkActionsHotKeys } = useBulkActionsHotKeys();
|
||||
const { conversationHotKeys } = useConversationHotKeys();
|
||||
|
||||
const placeholder = computed(() => t('COMMAND_BAR.SEARCH_PLACEHOLDER'));
|
||||
|
||||
const hotKeys = computed(() => [
|
||||
...inboxHotKeys.value,
|
||||
...goToCommandHotKeys.value,
|
||||
...goToAppearanceHotKeys.value,
|
||||
...bulkActionsHotKeys.value,
|
||||
...conversationHotKeys.value,
|
||||
]);
|
||||
|
||||
const setCommandBarData = () => {
|
||||
ninjakeys.value.data = hotKeys.value;
|
||||
};
|
||||
|
||||
const onSelected = item => {
|
||||
const {
|
||||
detail: { action: { title = null, section = null, id = null } = {} } = {},
|
||||
} = item;
|
||||
// Added this condition to prevent setting the selectedSnoozeType to null
|
||||
// When we select the "custom snooze" (CMD bar will close and the custom snooze modal will open)
|
||||
if (id === wootConstants.SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME) {
|
||||
selectedSnoozeType.value = wootConstants.SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME;
|
||||
} else {
|
||||
selectedSnoozeType.value = null;
|
||||
}
|
||||
|
||||
track(GENERAL_EVENTS.COMMAND_BAR, {
|
||||
section,
|
||||
action: title,
|
||||
});
|
||||
|
||||
setCommandBarData();
|
||||
};
|
||||
|
||||
const onClosed = () => {
|
||||
// If the selectedSnoozeType is not "SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME (custom snooze)" then we set the context menu chat id to null
|
||||
// Else we do nothing and its handled in the ChatList.vue hideCustomSnoozeModal() method
|
||||
if (
|
||||
selectedSnoozeType.value !== wootConstants.SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME
|
||||
) {
|
||||
store.dispatch('setContextMenuChatId', null);
|
||||
}
|
||||
};
|
||||
|
||||
watchEffect(() => {
|
||||
if (ninjakeys.value) {
|
||||
ninjakeys.value.data = hotKeys.value;
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(setCommandBarData);
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable vue/attribute-hyphenation -->
|
||||
|
||||
@@ -1,413 +0,0 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
import { CMD_AI_ASSIST } from './commandBarBusEvents';
|
||||
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
|
||||
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 './CommandBarIcons';
|
||||
|
||||
import {
|
||||
OPEN_CONVERSATION_ACTIONS,
|
||||
SNOOZE_CONVERSATION_ACTIONS,
|
||||
RESOLVED_CONVERSATION_ACTIONS,
|
||||
SEND_TRANSCRIPT_ACTION,
|
||||
UNMUTE_ACTION,
|
||||
MUTE_ACTION,
|
||||
} from './commandBarActions';
|
||||
import {
|
||||
isAConversationRoute,
|
||||
isAInboxViewRoute,
|
||||
} from '../../../helper/routeHelpers';
|
||||
export default {
|
||||
watch: {
|
||||
assignableAgents() {
|
||||
this.setCommandbarData();
|
||||
},
|
||||
currentChat() {
|
||||
this.setCommandbarData();
|
||||
},
|
||||
teamsList() {
|
||||
this.setCommandbarData();
|
||||
},
|
||||
activeLabels() {
|
||||
this.setCommandbarData();
|
||||
},
|
||||
draftMessage() {
|
||||
this.setCommandbarData();
|
||||
},
|
||||
replyMode() {
|
||||
this.setCommandbarData();
|
||||
},
|
||||
contextMenuChatId() {
|
||||
this.setCommandbarData();
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentChat: 'getSelectedChat',
|
||||
replyMode: 'draftMessages/getReplyEditorMode',
|
||||
contextMenuChatId: 'getContextMenuChatId',
|
||||
teams: 'teams/getTeams',
|
||||
}),
|
||||
draftMessage() {
|
||||
return this.$store.getters['draftMessages/get'](this.draftKey);
|
||||
},
|
||||
draftKey() {
|
||||
return `draft-${this.conversationId}-${this.replyMode}`;
|
||||
},
|
||||
inboxId() {
|
||||
return this.currentChat?.inbox_id;
|
||||
},
|
||||
conversationId() {
|
||||
return this.currentChat?.id;
|
||||
},
|
||||
hasAnAssignedTeam() {
|
||||
return !!this.currentChat?.meta?.team;
|
||||
},
|
||||
teamsList() {
|
||||
if (this.hasAnAssignedTeam) {
|
||||
return [
|
||||
{ id: 0, name: this.$t('TEAMS_SETTINGS.LIST.NONE') },
|
||||
...this.teams,
|
||||
];
|
||||
}
|
||||
return this.teams;
|
||||
},
|
||||
statusActions() {
|
||||
const isOpen =
|
||||
this.currentChat?.status === wootConstants.STATUS_TYPE.OPEN;
|
||||
const isSnoozed =
|
||||
this.currentChat?.status === wootConstants.STATUS_TYPE.SNOOZED;
|
||||
const isResolved =
|
||||
this.currentChat?.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 this.prepareActions(actions);
|
||||
},
|
||||
|
||||
priorityOptions() {
|
||||
return [
|
||||
{
|
||||
label: this.$t('CONVERSATION.PRIORITY.OPTIONS.NONE'),
|
||||
key: null,
|
||||
icon: ICON_PRIORITY_NONE,
|
||||
},
|
||||
{
|
||||
label: this.$t('CONVERSATION.PRIORITY.OPTIONS.URGENT'),
|
||||
key: 'urgent',
|
||||
icon: ICON_PRIORITY_URGENT,
|
||||
},
|
||||
{
|
||||
label: this.$t('CONVERSATION.PRIORITY.OPTIONS.HIGH'),
|
||||
key: 'high',
|
||||
icon: ICON_PRIORITY_HIGH,
|
||||
},
|
||||
{
|
||||
label: this.$t('CONVERSATION.PRIORITY.OPTIONS.MEDIUM'),
|
||||
key: 'medium',
|
||||
icon: ICON_PRIORITY_MEDIUM,
|
||||
},
|
||||
{
|
||||
label: this.$t('CONVERSATION.PRIORITY.OPTIONS.LOW'),
|
||||
key: 'low',
|
||||
icon: ICON_PRIORITY_LOW,
|
||||
},
|
||||
].filter(item => item.key !== this.currentChat?.priority);
|
||||
},
|
||||
assignAgentActions() {
|
||||
const agentOptions = this.agentsList.map(agent => ({
|
||||
id: `agent-${agent.id}`,
|
||||
title: agent.name,
|
||||
parent: 'assign_an_agent',
|
||||
section: this.$t('COMMAND_BAR.SECTIONS.CHANGE_ASSIGNEE'),
|
||||
agentInfo: agent,
|
||||
icon: ICON_ASSIGN_AGENT,
|
||||
handler: this.onChangeAssignee,
|
||||
}));
|
||||
return [
|
||||
{
|
||||
id: 'assign_an_agent',
|
||||
title: this.$t('COMMAND_BAR.COMMANDS.ASSIGN_AN_AGENT'),
|
||||
section: this.$t('COMMAND_BAR.SECTIONS.CONVERSATION'),
|
||||
icon: ICON_ASSIGN_AGENT,
|
||||
children: agentOptions.map(option => option.id),
|
||||
},
|
||||
...agentOptions,
|
||||
];
|
||||
},
|
||||
assignPriorityActions() {
|
||||
const options = this.priorityOptions.map(priority => ({
|
||||
id: `priority-${priority.key}`,
|
||||
title: priority.label,
|
||||
parent: 'assign_priority',
|
||||
section: this.$t('COMMAND_BAR.SECTIONS.CHANGE_PRIORITY'),
|
||||
priority: priority,
|
||||
icon: priority.icon,
|
||||
handler: this.onChangePriority,
|
||||
}));
|
||||
return [
|
||||
{
|
||||
id: 'assign_priority',
|
||||
title: this.$t('COMMAND_BAR.COMMANDS.ASSIGN_PRIORITY'),
|
||||
section: this.$t('COMMAND_BAR.SECTIONS.CONVERSATION'),
|
||||
icon: ICON_ASSIGN_PRIORITY,
|
||||
children: options.map(option => option.id),
|
||||
},
|
||||
...options,
|
||||
];
|
||||
},
|
||||
assignTeamActions() {
|
||||
const teamOptions = this.teamsList.map(team => ({
|
||||
id: `team-${team.id}`,
|
||||
title: team.name,
|
||||
parent: 'assign_a_team',
|
||||
section: this.$t('COMMAND_BAR.SECTIONS.CHANGE_TEAM'),
|
||||
teamInfo: team,
|
||||
icon: ICON_ASSIGN_TEAM,
|
||||
handler: this.onChangeTeam,
|
||||
}));
|
||||
return [
|
||||
{
|
||||
id: 'assign_a_team',
|
||||
title: this.$t('COMMAND_BAR.COMMANDS.ASSIGN_A_TEAM'),
|
||||
section: this.$t('COMMAND_BAR.SECTIONS.CONVERSATION'),
|
||||
icon: ICON_ASSIGN_TEAM,
|
||||
children: teamOptions.map(option => option.id),
|
||||
},
|
||||
...teamOptions,
|
||||
];
|
||||
},
|
||||
|
||||
addLabelActions() {
|
||||
const availableLabels = this.inactiveLabels.map(label => ({
|
||||
id: label.title,
|
||||
title: `#${label.title}`,
|
||||
parent: 'add_a_label_to_the_conversation',
|
||||
section: this.$t('COMMAND_BAR.SECTIONS.ADD_LABEL'),
|
||||
icon: ICON_ADD_LABEL,
|
||||
handler: action => this.addLabelToConversation({ title: action.id }),
|
||||
}));
|
||||
return [
|
||||
...availableLabels,
|
||||
{
|
||||
id: 'add_a_label_to_the_conversation',
|
||||
title: this.$t('COMMAND_BAR.COMMANDS.ADD_LABELS_TO_CONVERSATION'),
|
||||
section: this.$t('COMMAND_BAR.SECTIONS.CONVERSATION'),
|
||||
icon: ICON_ADD_LABEL,
|
||||
children: this.inactiveLabels.map(label => label.title),
|
||||
},
|
||||
];
|
||||
},
|
||||
removeLabelActions() {
|
||||
const activeLabels = this.activeLabels.map(label => ({
|
||||
id: label.title,
|
||||
title: `#${label.title}`,
|
||||
parent: 'remove_a_label_to_the_conversation',
|
||||
section: this.$t('COMMAND_BAR.SECTIONS.REMOVE_LABEL'),
|
||||
icon: ICON_REMOVE_LABEL,
|
||||
handler: action => this.removeLabelFromConversation(action.id),
|
||||
}));
|
||||
return [
|
||||
...activeLabels,
|
||||
{
|
||||
id: 'remove_a_label_to_the_conversation',
|
||||
title: this.$t('COMMAND_BAR.COMMANDS.REMOVE_LABEL_FROM_CONVERSATION'),
|
||||
section: this.$t('COMMAND_BAR.SECTIONS.CONVERSATION'),
|
||||
icon: ICON_REMOVE_LABEL,
|
||||
children: this.activeLabels.map(label => label.title),
|
||||
},
|
||||
];
|
||||
},
|
||||
labelActions() {
|
||||
if (this.activeLabels.length) {
|
||||
return [...this.addLabelActions, ...this.removeLabelActions];
|
||||
}
|
||||
return this.addLabelActions;
|
||||
},
|
||||
conversationAdditionalActions() {
|
||||
return this.prepareActions([
|
||||
this.currentChat.muted ? UNMUTE_ACTION : MUTE_ACTION,
|
||||
SEND_TRANSCRIPT_ACTION,
|
||||
]);
|
||||
},
|
||||
|
||||
nonDraftMessageAIAssistActions() {
|
||||
if (this.replyMode === REPLY_EDITOR_MODES.REPLY) {
|
||||
return [
|
||||
{
|
||||
label: this.$t(
|
||||
'INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.REPLY_SUGGESTION'
|
||||
),
|
||||
key: 'reply_suggestion',
|
||||
icon: ICON_AI_ASSIST,
|
||||
},
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
label: this.$t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.SUMMARIZE'),
|
||||
key: 'summarize',
|
||||
icon: ICON_AI_SUMMARY,
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
draftMessageAIAssistActions() {
|
||||
return [
|
||||
{
|
||||
label: this.$t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.REPHRASE'),
|
||||
key: 'rephrase',
|
||||
icon: ICON_AI_ASSIST,
|
||||
},
|
||||
{
|
||||
label: this.$t(
|
||||
'INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.FIX_SPELLING_GRAMMAR'
|
||||
),
|
||||
key: 'fix_spelling_grammar',
|
||||
icon: ICON_AI_GRAMMAR,
|
||||
},
|
||||
{
|
||||
label: this.$t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.EXPAND'),
|
||||
key: 'expand',
|
||||
icon: ICON_AI_EXPAND,
|
||||
},
|
||||
{
|
||||
label: this.$t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.SHORTEN'),
|
||||
key: 'shorten',
|
||||
icon: ICON_AI_SHORTEN,
|
||||
},
|
||||
{
|
||||
label: this.$t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.MAKE_FRIENDLY'),
|
||||
key: 'make_friendly',
|
||||
icon: ICON_AI_ASSIST,
|
||||
},
|
||||
{
|
||||
label: this.$t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.MAKE_FORMAL'),
|
||||
key: 'make_formal',
|
||||
icon: ICON_AI_ASSIST,
|
||||
},
|
||||
{
|
||||
label: this.$t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.SIMPLIFY'),
|
||||
key: 'simplify',
|
||||
icon: ICON_AI_ASSIST,
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
AIAssistActions() {
|
||||
const aiOptions = this.draftMessage
|
||||
? this.draftMessageAIAssistActions
|
||||
: this.nonDraftMessageAIAssistActions;
|
||||
const options = aiOptions.map(item => ({
|
||||
id: `ai-assist-${item.key}`,
|
||||
title: item.label,
|
||||
parent: 'ai_assist',
|
||||
section: this.$t('COMMAND_BAR.SECTIONS.AI_ASSIST'),
|
||||
priority: item,
|
||||
icon: item.icon,
|
||||
handler: () => emitter.emit(CMD_AI_ASSIST, item.key),
|
||||
}));
|
||||
return [
|
||||
{
|
||||
id: 'ai_assist',
|
||||
title: this.$t('COMMAND_BAR.COMMANDS.AI_ASSIST'),
|
||||
section: this.$t('COMMAND_BAR.SECTIONS.AI_ASSIST'),
|
||||
icon: ICON_AI_ASSIST,
|
||||
children: options.map(option => option.id),
|
||||
},
|
||||
...options,
|
||||
];
|
||||
},
|
||||
|
||||
isConversationOrInboxRoute() {
|
||||
return (
|
||||
isAConversationRoute(this.$route.name) ||
|
||||
isAInboxViewRoute(this.$route.name)
|
||||
);
|
||||
},
|
||||
|
||||
shouldShowSnoozeOption() {
|
||||
return (
|
||||
isAConversationRoute(this.$route.name, true, false) &&
|
||||
this.contextMenuChatId
|
||||
);
|
||||
},
|
||||
|
||||
getDefaultConversationHotKeys() {
|
||||
const defaultConversationHotKeys = [
|
||||
...this.statusActions,
|
||||
...this.conversationAdditionalActions,
|
||||
...this.assignAgentActions,
|
||||
...this.assignTeamActions,
|
||||
...this.labelActions,
|
||||
...this.assignPriorityActions,
|
||||
];
|
||||
if (this.isAIIntegrationEnabled) {
|
||||
return [...defaultConversationHotKeys, ...this.AIAssistActions];
|
||||
}
|
||||
return defaultConversationHotKeys;
|
||||
},
|
||||
|
||||
conversationHotKeys() {
|
||||
if (this.shouldShowSnoozeOption) {
|
||||
return this.prepareActions(SNOOZE_CONVERSATION_ACTIONS);
|
||||
}
|
||||
if (this.isConversationOrInboxRoute) {
|
||||
return this.getDefaultConversationHotKeys;
|
||||
}
|
||||
return [];
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onChangeAssignee(action) {
|
||||
this.$store.dispatch('assignAgent', {
|
||||
conversationId: this.currentChat.id,
|
||||
agentId: action.agentInfo.id,
|
||||
});
|
||||
},
|
||||
onChangePriority(action) {
|
||||
this.$store.dispatch('assignPriority', {
|
||||
conversationId: this.currentChat.id,
|
||||
priority: action.priority.key,
|
||||
});
|
||||
},
|
||||
onChangeTeam(action) {
|
||||
this.$store.dispatch('assignTeam', {
|
||||
conversationId: this.currentChat.id,
|
||||
teamId: action.teamInfo.id,
|
||||
});
|
||||
},
|
||||
prepareActions(actions) {
|
||||
return actions.map(action => ({
|
||||
...action,
|
||||
title: this.$t(action.title),
|
||||
section: this.$t(action.section),
|
||||
}));
|
||||
},
|
||||
},
|
||||
};
|
||||
+19
-4
@@ -4,7 +4,7 @@ import FilterInputBox from '../../../../components/widgets/FilterInput/Index.vue
|
||||
import countries from 'shared/constants/countries.js';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { filterAttributeGroups } from '../contactFilterItems';
|
||||
import filterMixin from 'shared/mixins/filterMixin';
|
||||
import { useFilter } from 'shared/composables/useFilter';
|
||||
import * as OPERATORS from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js';
|
||||
import { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||
import { validateConversationOrContactFilters } from 'dashboard/helper/validations.js';
|
||||
@@ -13,7 +13,6 @@ export default {
|
||||
components: {
|
||||
FilterInputBox,
|
||||
},
|
||||
mixins: [filterMixin],
|
||||
props: {
|
||||
onClose: {
|
||||
type: Function,
|
||||
@@ -36,6 +35,15 @@ export default {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { setFilterAttributes } = useFilter({
|
||||
filteri18nKey: 'CONTACTS_FILTER',
|
||||
attributeModel: 'contact_attribute',
|
||||
});
|
||||
return {
|
||||
setFilterAttributes,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: true,
|
||||
@@ -69,7 +77,10 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setFilterAttributes();
|
||||
const { filterGroups, filterTypes } = this.setFilterAttributes();
|
||||
this.filterTypes = [...this.filterTypes, ...filterTypes];
|
||||
this.filterGroups = filterGroups;
|
||||
|
||||
if (this.getAppliedContactFilters.length) {
|
||||
this.appliedFilters = [...this.getAppliedContactFilters];
|
||||
} else if (!this.isSegmentsView) {
|
||||
@@ -297,7 +308,11 @@ export default {
|
||||
:dropdown-values="getDropdownValues(appliedFilters[i].attribute_key)"
|
||||
:show-query-operator="i !== appliedFilters.length - 1"
|
||||
:show-user-input="showUserInput(appliedFilters[i].filter_operator)"
|
||||
:error-message="validationErrors[`filter_${i}`]"
|
||||
:error-message="
|
||||
validationErrors[`filter_${i}`]
|
||||
? $t(`CONTACTS_FILTER.ERRORS.VALUE_REQUIRED`)
|
||||
: ''
|
||||
"
|
||||
@resetFilter="resetFilter(i, appliedFilters[i])"
|
||||
@removeFilter="removeFilter(i)"
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import SwitchLayout from './SwitchLayout.vue';
|
||||
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||
export default {
|
||||
@@ -14,7 +13,6 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
isOnExpandedLayout: {
|
||||
type: Boolean,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { getUnixTime } from 'date-fns';
|
||||
import { CMD_SNOOZE_NOTIFICATION } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
|
||||
import { CMD_SNOOZE_NOTIFICATION } from 'dashboard/helper/commandbar/events';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { findSnoozeTime } from 'dashboard/helper/snoozeHelpers';
|
||||
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
|
||||
@@ -22,17 +22,21 @@ defineProps({
|
||||
<template>
|
||||
<div class="flex flex-col w-full h-full gap-10 font-inter">
|
||||
<slot name="header" />
|
||||
<slot v-if="isLoading" name="loading">
|
||||
<woot-loading-state :message="loadingMessage" />
|
||||
</slot>
|
||||
<p
|
||||
v-else-if="noRecordsFound"
|
||||
class="flex-1 text-slate-700 dark:text-slate-100 flex items-center justify-center text-base"
|
||||
>
|
||||
{{ noRecordsMessage }}
|
||||
</p>
|
||||
<slot v-else name="body" />
|
||||
<!-- Do not delete the slot below. It is required to render anything that is not defined in the above slots. -->
|
||||
<slot />
|
||||
<!-- Added to render any templates that should be rendered before body -->
|
||||
<div>
|
||||
<slot name="preBody" />
|
||||
<slot v-if="isLoading" name="loading">
|
||||
<woot-loading-state :message="loadingMessage" />
|
||||
</slot>
|
||||
<p
|
||||
v-else-if="noRecordsFound"
|
||||
class="flex-1 text-slate-700 dark:text-slate-100 flex items-center justify-center text-base"
|
||||
>
|
||||
{{ noRecordsMessage }}
|
||||
</p>
|
||||
<slot v-else name="body" />
|
||||
<!-- Do not delete the slot below. It is required to render anything that is not defined in the above slots. -->
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -12,6 +12,12 @@ export default {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
// Passes 0 or 1 based on the selected AttributeModel tab selected in the UI
|
||||
// Needs a better data type, todo: refactor this component later
|
||||
selectedAttributeModelTab: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
@@ -20,7 +26,10 @@ export default {
|
||||
return {
|
||||
displayName: '',
|
||||
description: '',
|
||||
attributeModel: 0,
|
||||
// Using the prop as default. There is no side effect here as the component
|
||||
// is destroyed completely when the modal is closed. The prop doesn't change
|
||||
// dynamically when the modal is active.
|
||||
attributeModel: this.selectedAttributeModelTab || 0,
|
||||
attributeType: 0,
|
||||
attributeKey: '',
|
||||
regexPattern: null,
|
||||
@@ -280,13 +289,16 @@ export default {
|
||||
padding: 0 var(--space-small) var(--space-small) 0;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.multiselect--wrap {
|
||||
margin-bottom: var(--space-normal);
|
||||
|
||||
.error-message {
|
||||
color: var(--r-400);
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-normal);
|
||||
}
|
||||
|
||||
.invalid {
|
||||
::v-deep {
|
||||
.multiselect__tags {
|
||||
@@ -295,13 +307,16 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep {
|
||||
.multiselect {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.multiselect__content-wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.multiselect--active .multiselect__tags {
|
||||
border-radius: var(--border-radius-normal);
|
||||
}
|
||||
|
||||
+129
-196
@@ -1,195 +1,141 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
<script setup>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import EditAttribute from './EditAttribute.vue';
|
||||
import { useStoreGetters, useStore } from 'dashboard/composables/store';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
const props = defineProps({
|
||||
attributeModel: {
|
||||
type: String,
|
||||
default: 'conversation_attribute',
|
||||
},
|
||||
});
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EditAttribute,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedTabIndex: 0,
|
||||
showEditPopup: false,
|
||||
showDeletePopup: false,
|
||||
selectedAttribute: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
uiFlags: 'attributes/getUIFlags',
|
||||
}),
|
||||
attributes() {
|
||||
const attributeModel = this.selectedTabIndex
|
||||
? 'contact_attribute'
|
||||
: 'conversation_attribute';
|
||||
const { t } = useI18n();
|
||||
|
||||
return this.$store.getters['attributes/getAttributesByModel'](
|
||||
attributeModel
|
||||
);
|
||||
},
|
||||
tabs() {
|
||||
return [
|
||||
{
|
||||
key: 0,
|
||||
name: this.$t('ATTRIBUTES_MGMT.TABS.CONVERSATION'),
|
||||
},
|
||||
{
|
||||
key: 1,
|
||||
name: this.$t('ATTRIBUTES_MGMT.TABS.CONTACT'),
|
||||
},
|
||||
];
|
||||
},
|
||||
deleteConfirmText() {
|
||||
return `${this.$t('ATTRIBUTES_MGMT.DELETE.CONFIRM.YES')} ${
|
||||
this.selectedAttribute.attribute_display_name
|
||||
}`;
|
||||
},
|
||||
deleteRejectText() {
|
||||
return this.$t('ATTRIBUTES_MGMT.DELETE.CONFIRM.NO');
|
||||
},
|
||||
confirmDeleteTitle() {
|
||||
return this.$t('ATTRIBUTES_MGMT.DELETE.CONFIRM.TITLE', {
|
||||
attributeName: this.selectedAttribute.attribute_display_name,
|
||||
});
|
||||
},
|
||||
confirmPlaceHolderText() {
|
||||
return `${this.$t('ATTRIBUTES_MGMT.DELETE.CONFIRM.PLACE_HOLDER', {
|
||||
attributeName: this.selectedAttribute.attribute_display_name,
|
||||
})}`;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchAttributes(this.selectedTabIndex);
|
||||
},
|
||||
methods: {
|
||||
onClickTabChange(index) {
|
||||
this.selectedTabIndex = index;
|
||||
this.fetchAttributes(index);
|
||||
},
|
||||
fetchAttributes(index) {
|
||||
this.$store.dispatch('attributes/get', index);
|
||||
},
|
||||
async deleteAttributes({ id }) {
|
||||
try {
|
||||
await this.$store.dispatch('attributes/delete', id);
|
||||
useAlert(this.$t('ATTRIBUTES_MGMT.DELETE.API.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.response?.message ||
|
||||
this.$t('ATTRIBUTES_MGMT.DELETE.API.ERROR_MESSAGE');
|
||||
useAlert(errorMessage);
|
||||
}
|
||||
},
|
||||
openEditPopup(response) {
|
||||
this.showEditPopup = true;
|
||||
this.selectedAttribute = response;
|
||||
},
|
||||
hideEditPopup() {
|
||||
this.showEditPopup = false;
|
||||
},
|
||||
confirmDeletion() {
|
||||
this.deleteAttributes(this.selectedAttribute);
|
||||
this.closeDelete();
|
||||
},
|
||||
openDelete(value) {
|
||||
this.showDeletePopup = true;
|
||||
this.selectedAttribute = value;
|
||||
},
|
||||
closeDelete() {
|
||||
this.showDeletePopup = false;
|
||||
this.selectedAttribute = {};
|
||||
},
|
||||
},
|
||||
const showEditPopup = ref(false);
|
||||
const showDeletePopup = ref(false);
|
||||
const selectedAttribute = ref({});
|
||||
|
||||
const getters = useStoreGetters();
|
||||
const store = useStore();
|
||||
|
||||
const attributes = computed(() =>
|
||||
getters['attributes/getAttributesByModel'].value(props.attributeModel)
|
||||
);
|
||||
const uiFlags = computed(() => getters['attributes/getUIFlags'].value);
|
||||
|
||||
const attributeDisplayName = computed(
|
||||
() => selectedAttribute.value.attribute_display_name
|
||||
);
|
||||
const deleteConfirmText = computed(
|
||||
() =>
|
||||
`${t('ATTRIBUTES_MGMT.DELETE.CONFIRM.YES')} ${attributeDisplayName.value}`
|
||||
);
|
||||
const deleteRejectText = computed(() => t('ATTRIBUTES_MGMT.DELETE.CONFIRM.NO'));
|
||||
const confirmDeleteTitle = computed(() =>
|
||||
t('ATTRIBUTES_MGMT.DELETE.CONFIRM.TITLE', {
|
||||
attributeName: attributeDisplayName.value,
|
||||
})
|
||||
);
|
||||
const confirmPlaceHolderText = computed(
|
||||
() =>
|
||||
`${t('ATTRIBUTES_MGMT.DELETE.CONFIRM.PLACE_HOLDER', {
|
||||
attributeName: attributeDisplayName.value,
|
||||
})}`
|
||||
);
|
||||
|
||||
const deleteAttributes = async ({ id }) => {
|
||||
try {
|
||||
await store.dispatch('attributes/delete', id);
|
||||
useAlert(t('ATTRIBUTES_MGMT.DELETE.API.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.response?.message || t('ATTRIBUTES_MGMT.DELETE.API.ERROR_MESSAGE');
|
||||
useAlert(errorMessage);
|
||||
}
|
||||
};
|
||||
const openEditPopup = response => {
|
||||
showEditPopup.value = true;
|
||||
selectedAttribute.value = response;
|
||||
};
|
||||
const hideEditPopup = () => {
|
||||
showEditPopup.value = false;
|
||||
};
|
||||
|
||||
const closeDelete = () => {
|
||||
showDeletePopup.value = false;
|
||||
selectedAttribute.value = {};
|
||||
};
|
||||
const confirmDeletion = () => {
|
||||
deleteAttributes(selectedAttribute.value);
|
||||
closeDelete();
|
||||
};
|
||||
const openDelete = value => {
|
||||
showDeletePopup.value = true;
|
||||
selectedAttribute.value = value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-row gap-4 p-8">
|
||||
<div class="w-full lg:w-3/5">
|
||||
<woot-tabs :index="selectedTabIndex" @change="onClickTabChange">
|
||||
<woot-tabs-item
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
:name="tab.name"
|
||||
:show-badge="false"
|
||||
/>
|
||||
</woot-tabs>
|
||||
|
||||
<div class="w-full">
|
||||
<p
|
||||
v-if="!uiFlags.isFetching && !attributes.length"
|
||||
class="flex items-center justify-center mt-12"
|
||||
<div class="flex flex-col">
|
||||
<table class="min-w-full overflow-x-auto">
|
||||
<thead>
|
||||
<th
|
||||
v-for="tableHeader in $t('ATTRIBUTES_MGMT.LIST.TABLE_HEADER')"
|
||||
:key="tableHeader"
|
||||
class="py-4 ltr:pr-4 rtl:pl-4 text-left font-semibold text-slate-700 dark:text-slate-300"
|
||||
>
|
||||
{{ $t('ATTRIBUTES_MGMT.LIST.EMPTY_RESULT.404') }}
|
||||
</p>
|
||||
<woot-loading-state
|
||||
v-if="uiFlags.isFetching"
|
||||
:message="$t('ATTRIBUTES_MGMT.LOADING')"
|
||||
/>
|
||||
<table
|
||||
v-if="!uiFlags.isFetching && attributes.length"
|
||||
class="w-full mt-2 table-fixed woot-table"
|
||||
>
|
||||
<thead>
|
||||
<th
|
||||
v-for="tableHeader in $t('ATTRIBUTES_MGMT.LIST.TABLE_HEADER')"
|
||||
:key="tableHeader"
|
||||
class="pl-0 max-w-[6.25rem] min-w-[5rem]"
|
||||
>
|
||||
{{ tableHeader }}
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="attribute in attributes" :key="attribute.attribute_key">
|
||||
<td
|
||||
class="pl-0 max-w-[6.25rem] min-w-[5rem] overflow-hidden whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ attribute.attribute_display_name }}
|
||||
</td>
|
||||
<td
|
||||
class="pl-0 max-w-[10rem] min-w-[6.25rem] overflow-hidden whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ attribute.attribute_description }}
|
||||
</td>
|
||||
<td
|
||||
class="pl-0 max-w-[6.25rem] min-w-[5rem] overflow-hidden whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ attribute.attribute_display_type }}
|
||||
</td>
|
||||
<td
|
||||
class="attribute-key pl-0 max-w-[6.25rem] min-w-[5rem] overflow-hidden whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ attribute.attribute_key }}
|
||||
</td>
|
||||
<td class="button-wrapper">
|
||||
<woot-button
|
||||
v-tooltip.top="$t('ATTRIBUTES_MGMT.LIST.BUTTONS.EDIT')"
|
||||
variant="smooth"
|
||||
size="tiny"
|
||||
color-scheme="secondary"
|
||||
class-names="grey-btn"
|
||||
icon="edit"
|
||||
@click="openEditPopup(attribute)"
|
||||
/>
|
||||
<woot-button
|
||||
v-tooltip.top="$t('ATTRIBUTES_MGMT.LIST.BUTTONS.DELETE')"
|
||||
variant="smooth"
|
||||
color-scheme="alert"
|
||||
size="tiny"
|
||||
icon="dismiss-circle"
|
||||
class-names="grey-btn"
|
||||
@click="openDelete(attribute)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden w-1/3 lg:block">
|
||||
<span v-dompurify-html="$t('ATTRIBUTES_MGMT.SIDEBAR_TXT')" />
|
||||
</div>
|
||||
{{ tableHeader }}
|
||||
</th>
|
||||
</thead>
|
||||
<tbody
|
||||
class="divide-y divide-slate-25 dark:divide-slate-800 flex-1 text-slate-700 dark:text-slate-100"
|
||||
>
|
||||
<tr v-for="attribute in attributes" :key="attribute.attribute_key">
|
||||
<td
|
||||
class="py-4 ltr:pr-4 rtl:pl-4 overflow-hidden whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ attribute.attribute_display_name }}
|
||||
</td>
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4">
|
||||
{{ attribute.attribute_description }}
|
||||
</td>
|
||||
<td
|
||||
class="py-4 ltr:pr-4 rtl:pl-4 overflow-hidden whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ attribute.attribute_display_type }}
|
||||
</td>
|
||||
<td
|
||||
class="py-4 ltr:pr-4 rtl:pl-4 attribute-key overflow-hidden whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ attribute.attribute_key }}
|
||||
</td>
|
||||
<td class="py-4 min-w-xs">
|
||||
<div class="flex gap-1">
|
||||
<woot-button
|
||||
v-tooltip.top="$t('ATTRIBUTES_MGMT.LIST.BUTTONS.EDIT')"
|
||||
variant="smooth"
|
||||
size="tiny"
|
||||
color-scheme="secondary"
|
||||
class-names="grey-btn"
|
||||
icon="edit"
|
||||
@click="openEditPopup(attribute)"
|
||||
/>
|
||||
<woot-button
|
||||
v-tooltip.top="$t('ATTRIBUTES_MGMT.LIST.BUTTONS.DELETE')"
|
||||
variant="smooth"
|
||||
color-scheme="alert"
|
||||
size="tiny"
|
||||
icon="dismiss-circle"
|
||||
class-names="grey-btn"
|
||||
@click="openDelete(attribute)"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<woot-modal :show.sync="showEditPopup" :on-close="hideEditPopup">
|
||||
<EditAttribute
|
||||
:selected-attribute="selectedAttribute"
|
||||
@@ -216,17 +162,4 @@ export default {
|
||||
.attribute-key {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
::v-deep {
|
||||
.tabs--container {
|
||||
.tabs {
|
||||
@apply p-0;
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-title a {
|
||||
font-weight: var(--font-weight-medium);
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,40 +1,112 @@
|
||||
<script>
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
import AddAttribute from './AddAttribute.vue';
|
||||
import CustomAttribute from './CustomAttribute.vue';
|
||||
export default {
|
||||
components: {
|
||||
AddAttribute,
|
||||
CustomAttribute,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showAddPopup: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
openAddPopup() {
|
||||
this.showAddPopup = true;
|
||||
import SettingsLayout from '../SettingsLayout.vue';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { useStoreGetters, useStore } from 'dashboard/composables/store';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const getters = useStoreGetters();
|
||||
const store = useStore();
|
||||
|
||||
const showAddPopup = ref(false);
|
||||
const selectedTabIndex = ref(0);
|
||||
const uiFlags = computed(() => getters['attributes/getUIFlags'].value);
|
||||
|
||||
const openAddPopup = () => {
|
||||
showAddPopup.value = true;
|
||||
};
|
||||
const hideAddPopup = () => {
|
||||
showAddPopup.value = false;
|
||||
};
|
||||
|
||||
const tabs = computed(() => {
|
||||
return [
|
||||
{
|
||||
key: 0,
|
||||
name: t('ATTRIBUTES_MGMT.TABS.CONVERSATION'),
|
||||
},
|
||||
hideAddPopup() {
|
||||
this.showAddPopup = false;
|
||||
{
|
||||
key: 1,
|
||||
name: t('ATTRIBUTES_MGMT.TABS.CONTACT'),
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('attributes/get');
|
||||
});
|
||||
|
||||
const attributeModel = computed(() =>
|
||||
selectedTabIndex.value ? 'contact_attribute' : 'conversation_attribute'
|
||||
);
|
||||
|
||||
const attributes = computed(() =>
|
||||
getters['attributes/getAttributesByModel'].value(attributeModel.value)
|
||||
);
|
||||
|
||||
const onClickTabChange = index => {
|
||||
selectedTabIndex.value = index;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-1 overflow-auto">
|
||||
<woot-button
|
||||
color-scheme="success"
|
||||
class-names="button--fixed-top"
|
||||
icon="add-circle"
|
||||
@click="openAddPopup()"
|
||||
<SettingsLayout
|
||||
:is-loading="uiFlags.isFetching"
|
||||
:loading-message="$t('ATTRIBUTES_MGMT.LOADING')"
|
||||
:no-records-found="!attributes.length"
|
||||
:no-records-message="$t('ATTRIBUTES_MGMT.LIST.EMPTY_RESULT.404')"
|
||||
>
|
||||
<template #header>
|
||||
<BaseSettingsHeader
|
||||
:title="$t('ATTRIBUTES_MGMT.HEADER')"
|
||||
:description="$t('ATTRIBUTES_MGMT.DESCRIPTION')"
|
||||
:link-text="$t('ATTRIBUTES_MGMT.LEARN_MORE')"
|
||||
feature-name="custom_attributes"
|
||||
>
|
||||
<template #actions>
|
||||
<woot-button
|
||||
class="button nice rounded-md"
|
||||
icon="add-circle"
|
||||
@click="openAddPopup"
|
||||
>
|
||||
{{ $t('ATTRIBUTES_MGMT.HEADER_BTN_TXT') }}
|
||||
</woot-button>
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
<template #preBody>
|
||||
<woot-tabs
|
||||
class="font-medium [&_.tabs]:p-0 mb-4"
|
||||
:index="selectedTabIndex"
|
||||
@change="onClickTabChange"
|
||||
>
|
||||
<woot-tabs-item
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
:name="tab.name"
|
||||
:show-badge="false"
|
||||
/>
|
||||
</woot-tabs>
|
||||
</template>
|
||||
<template #body>
|
||||
<CustomAttribute
|
||||
:key="attributeModel"
|
||||
:attribute-model="attributeModel"
|
||||
/>
|
||||
</template>
|
||||
<woot-modal
|
||||
v-if="showAddPopup"
|
||||
:show.sync="showAddPopup"
|
||||
:on-close="hideAddPopup"
|
||||
>
|
||||
{{ $t('ATTRIBUTES_MGMT.HEADER_BTN_TXT') }}
|
||||
</woot-button>
|
||||
<CustomAttribute />
|
||||
<woot-modal :show.sync="showAddPopup" :on-close="hideAddPopup">
|
||||
<AddAttribute :on-close="hideAddPopup" />
|
||||
<AddAttribute
|
||||
:on-close="hideAddPopup"
|
||||
:selected-attribute-model-tab="selectedTabIndex"
|
||||
/>
|
||||
</woot-modal>
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
const SettingsContent = () => import('../Wrapper.vue');
|
||||
const SettingsWrapper = () => import('../SettingsWrapper.vue');
|
||||
const AttributesHome = () => import('./Index.vue');
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/settings/custom-attributes'),
|
||||
component: SettingsContent,
|
||||
props: {
|
||||
headerTitle: 'ATTRIBUTES_MGMT.HEADER',
|
||||
icon: 'code',
|
||||
showNewButton: false,
|
||||
},
|
||||
component: SettingsWrapper,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
|
||||
+93
-17
@@ -1,9 +1,17 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import automationMethodsMixin from 'dashboard/mixins/automations/methodsMixin';
|
||||
import FilterInputBox from 'dashboard/components/widgets/FilterInput/Index.vue';
|
||||
import AutomationActionInput from 'dashboard/components/widgets/AutomationActionInput.vue';
|
||||
|
||||
import { useAutomation } from 'dashboard/composables/useAutomation';
|
||||
import { validateAutomation } from 'dashboard/helper/validations';
|
||||
import {
|
||||
generateAutomationPayload,
|
||||
getAttributes,
|
||||
getInputType,
|
||||
getOperators,
|
||||
getCustomAttributeType,
|
||||
showActionInput,
|
||||
} from 'dashboard/helper/automationHelper';
|
||||
import {
|
||||
AUTOMATION_RULE_EVENTS,
|
||||
AUTOMATION_ACTION_TYPES,
|
||||
@@ -14,13 +22,38 @@ export default {
|
||||
FilterInputBox,
|
||||
AutomationActionInput,
|
||||
},
|
||||
mixins: [automationMethodsMixin],
|
||||
props: {
|
||||
onClose: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const {
|
||||
onEventChange,
|
||||
getConditionDropdownValues,
|
||||
appendNewCondition,
|
||||
appendNewAction,
|
||||
removeFilter,
|
||||
removeAction,
|
||||
resetFilter,
|
||||
resetAction,
|
||||
getActionDropdownValues,
|
||||
manifestCustomAttributes,
|
||||
} = useAutomation();
|
||||
return {
|
||||
onEventChange,
|
||||
getConditionDropdownValues,
|
||||
appendNewCondition,
|
||||
appendNewAction,
|
||||
removeFilter,
|
||||
removeAction,
|
||||
resetFilter,
|
||||
resetAction,
|
||||
getActionDropdownValues,
|
||||
manifestCustomAttributes,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
automationTypes: JSON.parse(JSON.stringify(AUTOMATIONS)),
|
||||
@@ -82,12 +115,24 @@ export default {
|
||||
this.$store.dispatch('labels/get');
|
||||
this.$store.dispatch('campaigns/get');
|
||||
this.allCustomAttributes = this.$store.getters['attributes/getAttributes'];
|
||||
this.manifestCustomAttributes();
|
||||
this.manifestCustomAttributes(this.automationTypes);
|
||||
},
|
||||
methods: {
|
||||
getAttributes,
|
||||
getInputType,
|
||||
getOperators,
|
||||
getCustomAttributeType,
|
||||
showActionInput,
|
||||
isFeatureEnabled(flag) {
|
||||
return this.isFeatureEnabledonAccount(this.accountId, flag);
|
||||
},
|
||||
emitSaveAutomation() {
|
||||
this.errors = validateAutomation(this.automation);
|
||||
if (Object.keys(this.errors).length === 0) {
|
||||
const automation = generateAutomationPayload(this.automation);
|
||||
this.$emit('saveAutomation', automation, this.mode);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -121,7 +166,7 @@ export default {
|
||||
<select
|
||||
v-model="automation.event_name"
|
||||
class="m-0"
|
||||
@change="onEventChange()"
|
||||
@change="onEventChange(automation)"
|
||||
>
|
||||
<option
|
||||
v-for="event in automationRuleEvents"
|
||||
@@ -154,9 +199,26 @@ export default {
|
||||
v-for="(condition, i) in automation.conditions"
|
||||
:key="i"
|
||||
v-model="automation.conditions[i]"
|
||||
:filter-attributes="getAttributes(automation.event_name)"
|
||||
:input-type="getInputType(automation.conditions[i].attribute_key)"
|
||||
:operators="getOperators(automation.conditions[i].attribute_key)"
|
||||
:filter-attributes="
|
||||
getAttributes(automationTypes, automation.event_name)
|
||||
"
|
||||
:input-type="
|
||||
getInputType(
|
||||
allCustomAttributes,
|
||||
automationTypes,
|
||||
automation,
|
||||
automation.conditions[i].attribute_key
|
||||
)
|
||||
"
|
||||
:operators="
|
||||
getOperators(
|
||||
allCustomAttributes,
|
||||
automationTypes,
|
||||
automation,
|
||||
mode,
|
||||
automation.conditions[i].attribute_key
|
||||
)
|
||||
"
|
||||
:dropdown-values="
|
||||
getConditionDropdownValues(
|
||||
automation.conditions[i].attribute_key
|
||||
@@ -164,15 +226,26 @@ export default {
|
||||
"
|
||||
:show-query-operator="i !== automation.conditions.length - 1"
|
||||
:custom-attribute-type="
|
||||
getCustomAttributeType(automation.conditions[i].attribute_key)
|
||||
getCustomAttributeType(
|
||||
automationTypes,
|
||||
automation,
|
||||
automation.conditions[i].attribute_key
|
||||
)
|
||||
"
|
||||
:error-message="
|
||||
errors[`condition_${i}`]
|
||||
? $t(`AUTOMATION.ERRORS.${errors[`condition_${i}`]}`)
|
||||
: ''
|
||||
"
|
||||
@resetFilter="resetFilter(i, automation.conditions[i])"
|
||||
@removeFilter="removeFilter(i)"
|
||||
@resetFilter="
|
||||
resetFilter(
|
||||
automation,
|
||||
automationTypes,
|
||||
i,
|
||||
automation.conditions[i]
|
||||
)
|
||||
"
|
||||
@removeFilter="removeFilter(automation, i)"
|
||||
/>
|
||||
<div class="mt-4">
|
||||
<woot-button
|
||||
@@ -180,7 +253,7 @@ export default {
|
||||
color-scheme="success"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
@click="appendNewCondition"
|
||||
@click="appendNewCondition(automation)"
|
||||
>
|
||||
{{ $t('AUTOMATION.ADD.CONDITION_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
@@ -205,15 +278,18 @@ export default {
|
||||
getActionDropdownValues(automation.actions[i].action_name)
|
||||
"
|
||||
:show-action-input="
|
||||
showActionInput(automation.actions[i].action_name)
|
||||
showActionInput(
|
||||
automationActionTypes,
|
||||
automation.actions[i].action_name
|
||||
)
|
||||
"
|
||||
:error-message="
|
||||
errors[`action_${i}`]
|
||||
? $t(`AUTOMATION.ERRORS.${errors[`action_${i}`]}`)
|
||||
: ''
|
||||
"
|
||||
@resetAction="resetAction(i)"
|
||||
@removeAction="removeAction(i)"
|
||||
@resetAction="resetAction(automation, i)"
|
||||
@removeAction="removeAction(automation, i)"
|
||||
/>
|
||||
<div class="mt-4">
|
||||
<woot-button
|
||||
@@ -221,7 +297,7 @@ export default {
|
||||
color-scheme="success"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
@click="appendNewAction"
|
||||
@click="appendNewAction(automation)"
|
||||
>
|
||||
{{ $t('AUTOMATION.ADD.ACTION_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
@@ -234,7 +310,7 @@ export default {
|
||||
<woot-button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('AUTOMATION.ADD.CANCEL_BUTTON_TEXT') }}
|
||||
</woot-button>
|
||||
<woot-button @click="submitAutomation">
|
||||
<woot-button @click="emitSaveAutomation">
|
||||
{{ $t('AUTOMATION.ADD.SUBMIT') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
|
||||
+106
-17
@@ -1,8 +1,18 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import automationMethodsMixin from 'dashboard/mixins/automations/methodsMixin';
|
||||
import { useAutomation } from 'dashboard/composables/useAutomation';
|
||||
import FilterInputBox from 'dashboard/components/widgets/FilterInput/Index.vue';
|
||||
import AutomationActionInput from 'dashboard/components/widgets/AutomationActionInput.vue';
|
||||
import {
|
||||
getFileName,
|
||||
generateAutomationPayload,
|
||||
getAttributes,
|
||||
getInputType,
|
||||
getOperators,
|
||||
getCustomAttributeType,
|
||||
showActionInput,
|
||||
} from 'dashboard/helper/automationHelper';
|
||||
import { validateAutomation } from 'dashboard/helper/validations';
|
||||
|
||||
import {
|
||||
AUTOMATION_RULE_EVENTS,
|
||||
@@ -15,7 +25,6 @@ export default {
|
||||
FilterInputBox,
|
||||
AutomationActionInput,
|
||||
},
|
||||
mixins: [automationMethodsMixin],
|
||||
props: {
|
||||
onClose: {
|
||||
type: Function,
|
||||
@@ -26,6 +35,34 @@ export default {
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const {
|
||||
onEventChange,
|
||||
getConditionDropdownValues,
|
||||
appendNewCondition,
|
||||
appendNewAction,
|
||||
removeFilter,
|
||||
removeAction,
|
||||
resetFilter,
|
||||
resetAction,
|
||||
getActionDropdownValues,
|
||||
formatAutomation,
|
||||
manifestCustomAttributes,
|
||||
} = useAutomation();
|
||||
return {
|
||||
onEventChange,
|
||||
getConditionDropdownValues,
|
||||
appendNewCondition,
|
||||
appendNewAction,
|
||||
removeFilter,
|
||||
removeAction,
|
||||
resetFilter,
|
||||
resetAction,
|
||||
getActionDropdownValues,
|
||||
formatAutomation,
|
||||
manifestCustomAttributes,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
automationTypes: JSON.parse(JSON.stringify(AUTOMATIONS)),
|
||||
@@ -61,14 +98,33 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.manifestCustomAttributes();
|
||||
this.manifestCustomAttributes(this.automationTypes);
|
||||
this.allCustomAttributes = this.$store.getters['attributes/getAttributes'];
|
||||
this.formatAutomation(this.selectedResponse);
|
||||
|
||||
this.automation = this.formatAutomation(
|
||||
this.selectedResponse,
|
||||
this.allCustomAttributes,
|
||||
this.automationTypes,
|
||||
this.automationActionTypes
|
||||
);
|
||||
},
|
||||
methods: {
|
||||
getFileName,
|
||||
getAttributes,
|
||||
getInputType,
|
||||
getOperators,
|
||||
getCustomAttributeType,
|
||||
showActionInput,
|
||||
isFeatureEnabled(flag) {
|
||||
return this.isFeatureEnabledonAccount(this.accountId, flag);
|
||||
},
|
||||
emitSaveAutomation() {
|
||||
this.errors = validateAutomation(this.automation);
|
||||
if (Object.keys(this.errors).length === 0) {
|
||||
const automation = generateAutomationPayload(this.automation);
|
||||
this.$emit('saveAutomation', automation, this.mode);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -99,7 +155,10 @@ export default {
|
||||
<div class="event_wrapper">
|
||||
<label :class="{ error: errors.event_name }">
|
||||
{{ $t('AUTOMATION.ADD.FORM.EVENT.LABEL') }}
|
||||
<select v-model="automation.event_name" @change="onEventChange()">
|
||||
<select
|
||||
v-model="automation.event_name"
|
||||
@change="onEventChange(automation)"
|
||||
>
|
||||
<option
|
||||
v-for="event in automationRuleEvents"
|
||||
:key="event.key"
|
||||
@@ -125,16 +184,37 @@ export default {
|
||||
v-for="(condition, i) in automation.conditions"
|
||||
:key="i"
|
||||
v-model="automation.conditions[i]"
|
||||
:filter-attributes="getAttributes(automation.event_name)"
|
||||
:input-type="getInputType(automation.conditions[i].attribute_key)"
|
||||
:operators="getOperators(automation.conditions[i].attribute_key)"
|
||||
:filter-attributes="
|
||||
getAttributes(automationTypes, automation.event_name)
|
||||
"
|
||||
:input-type="
|
||||
getInputType(
|
||||
allCustomAttributes,
|
||||
automationTypes,
|
||||
automation,
|
||||
automation.conditions[i].attribute_key
|
||||
)
|
||||
"
|
||||
:operators="
|
||||
getOperators(
|
||||
allCustomAttributes,
|
||||
automationTypes,
|
||||
automation,
|
||||
mode,
|
||||
automation.conditions[i].attribute_key
|
||||
)
|
||||
"
|
||||
:dropdown-values="
|
||||
getConditionDropdownValues(
|
||||
automation.conditions[i].attribute_key
|
||||
)
|
||||
"
|
||||
:custom-attribute-type="
|
||||
getCustomAttributeType(automation.conditions[i].attribute_key)
|
||||
getCustomAttributeType(
|
||||
automationTypes,
|
||||
automation,
|
||||
automation.conditions[i].attribute_key
|
||||
)
|
||||
"
|
||||
:show-query-operator="i !== automation.conditions.length - 1"
|
||||
:error-message="
|
||||
@@ -142,8 +222,15 @@ export default {
|
||||
? $t(`AUTOMATION.ERRORS.${errors[`condition_${i}`]}`)
|
||||
: ''
|
||||
"
|
||||
@resetFilter="resetFilter(i, automation.conditions[i])"
|
||||
@removeFilter="removeFilter(i)"
|
||||
@resetFilter="
|
||||
resetFilter(
|
||||
automation,
|
||||
automationTypes,
|
||||
i,
|
||||
automation.conditions[i]
|
||||
)
|
||||
"
|
||||
@removeFilter="removeFilter(automation, i)"
|
||||
/>
|
||||
<div class="mt-4">
|
||||
<woot-button
|
||||
@@ -151,7 +238,7 @@ export default {
|
||||
color-scheme="success"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
@click="appendNewCondition"
|
||||
@click="appendNewCondition(automation)"
|
||||
>
|
||||
{{ $t('AUTOMATION.ADD.CONDITION_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
@@ -173,15 +260,17 @@ export default {
|
||||
v-model="automation.actions[i]"
|
||||
:action-types="automationActionTypes"
|
||||
:dropdown-values="getActionDropdownValues(action.action_name)"
|
||||
:show-action-input="showActionInput(action.action_name)"
|
||||
:show-action-input="
|
||||
showActionInput(automationActionTypes, action.action_name)
|
||||
"
|
||||
:error-message="
|
||||
errors[`action_${i}`]
|
||||
? $t(`AUTOMATION.ERRORS.${errors[`action_${i}`]}`)
|
||||
: ''
|
||||
"
|
||||
:initial-file-name="getFileName(action, automation.files)"
|
||||
@resetAction="resetAction(i)"
|
||||
@removeAction="removeAction(i)"
|
||||
@resetAction="resetAction(automation, i)"
|
||||
@removeAction="removeAction(automation, i)"
|
||||
/>
|
||||
<div class="mt-4">
|
||||
<woot-button
|
||||
@@ -189,7 +278,7 @@ export default {
|
||||
color-scheme="success"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
@click="appendNewAction"
|
||||
@click="appendNewAction(automation)"
|
||||
>
|
||||
{{ $t('AUTOMATION.ADD.ACTION_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
@@ -206,7 +295,7 @@ export default {
|
||||
>
|
||||
{{ $t('AUTOMATION.EDIT.CANCEL_BUTTON_TEXT') }}
|
||||
</woot-button>
|
||||
<woot-button @click="submitAutomation">
|
||||
<woot-button @click="emitSaveAutomation">
|
||||
{{ $t('AUTOMATION.EDIT.SUBMIT') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
@@ -7,12 +7,12 @@ import BillingItem from './components/BillingItem.vue';
|
||||
|
||||
export default {
|
||||
components: { BillingItem },
|
||||
mixins: [messageFormatterMixin],
|
||||
setup() {
|
||||
const { accountId } = useAccount();
|
||||
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
return {
|
||||
accountId,
|
||||
formatMessage,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useVuelidate } from '@vuelidate/core';
|
||||
import { required } from '@vuelidate/validators';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||
import campaignMixin from 'shared/mixins/campaignMixin';
|
||||
import { useCampaign } from 'shared/composables/useCampaign';
|
||||
import WootDateTimePicker from 'dashboard/components/ui/DateTimePicker.vue';
|
||||
import { URLPattern } from 'urlpattern-polyfill';
|
||||
import { CAMPAIGNS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||
@@ -14,9 +14,9 @@ export default {
|
||||
WootDateTimePicker,
|
||||
WootMessageEditor,
|
||||
},
|
||||
mixins: [campaignMixin],
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
const { campaignType, isOngoingType, isOneOffType } = useCampaign();
|
||||
return { v$: useVuelidate(), campaignType, isOngoingType, isOneOffType };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import campaignMixin from 'shared/mixins/campaignMixin';
|
||||
import { useCampaign } from 'shared/composables/useCampaign';
|
||||
import CampaignsTable from './CampaignsTable.vue';
|
||||
import EditCampaign from './EditCampaign.vue';
|
||||
export default {
|
||||
@@ -9,13 +9,16 @@ export default {
|
||||
CampaignsTable,
|
||||
EditCampaign,
|
||||
},
|
||||
mixins: [campaignMixin],
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { campaignType } = useCampaign();
|
||||
return { campaignType };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showEditPopup: false,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import UserAvatarWithName from 'dashboard/components/widgets/UserAvatarWithName.vue';
|
||||
import InboxName from 'dashboard/components/widgets/InboxName.vue';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import { messageStamp } from 'shared/helpers/timeHelper';
|
||||
|
||||
export default {
|
||||
@@ -9,7 +9,6 @@ export default {
|
||||
UserAvatarWithName,
|
||||
InboxName,
|
||||
},
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
campaign: {
|
||||
type: Object,
|
||||
@@ -20,7 +19,12 @@ export default {
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
setup() {
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
return {
|
||||
formatMessage,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
campaignStatus() {
|
||||
if (this.isOngoingType) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
||||
import campaignMixin from 'shared/mixins/campaignMixin';
|
||||
import { useCampaign } from 'shared/composables/useCampaign';
|
||||
import CampaignCard from './CampaignCard.vue';
|
||||
|
||||
export default {
|
||||
@@ -10,9 +10,6 @@ export default {
|
||||
Spinner,
|
||||
CampaignCard,
|
||||
},
|
||||
|
||||
mixins: [campaignMixin],
|
||||
|
||||
props: {
|
||||
campaigns: {
|
||||
type: Array,
|
||||
@@ -27,7 +24,10 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
setup() {
|
||||
const { isOngoingType } = useCampaign();
|
||||
return { isOngoingType };
|
||||
},
|
||||
computed: {
|
||||
currentInboxId() {
|
||||
return this.$route.params.inboxId;
|
||||
|
||||
@@ -4,14 +4,13 @@ import { useVuelidate } from '@vuelidate/core';
|
||||
import { required } from '@vuelidate/validators';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||
import campaignMixin from 'shared/mixins/campaignMixin';
|
||||
import { useCampaign } from 'shared/composables/useCampaign';
|
||||
import { URLPattern } from 'urlpattern-polyfill';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WootMessageEditor,
|
||||
},
|
||||
mixins: [campaignMixin],
|
||||
props: {
|
||||
selectedCampaign: {
|
||||
type: Object,
|
||||
@@ -19,7 +18,8 @@ export default {
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
const { isOngoingType } = useCampaign();
|
||||
return { v$: useVuelidate(), isOngoingType };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import campaignMixin from 'shared/mixins/campaignMixin';
|
||||
import { useCampaign } from 'shared/composables/useCampaign';
|
||||
import Campaign from './Campaign.vue';
|
||||
import AddCampaign from './AddCampaign.vue';
|
||||
|
||||
@@ -8,7 +8,10 @@ export default {
|
||||
Campaign,
|
||||
AddCampaign,
|
||||
},
|
||||
mixins: [campaignMixin],
|
||||
setup() {
|
||||
const { isOngoingType } = useCampaign();
|
||||
return { isOngoingType };
|
||||
},
|
||||
data() {
|
||||
return { showAddPopup: false };
|
||||
},
|
||||
|
||||
@@ -1,219 +1,177 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
<script setup>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import Settings from './Settings.vue';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import SettingsLayout from '../SettingsLayout.vue';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { useStoreGetters, useStore } from 'dashboard/composables/store';
|
||||
import ChannelName from './components/ChannelName.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Settings,
|
||||
},
|
||||
mixins: [globalConfigMixin],
|
||||
setup() {
|
||||
const { isAdmin } = useAdmin();
|
||||
const { accountScopedUrl } = useAccount();
|
||||
return {
|
||||
isAdmin,
|
||||
accountScopedUrl,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: {},
|
||||
showSettings: false,
|
||||
showDeletePopup: false,
|
||||
selectedInbox: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
inboxesList: 'inboxes/getInboxes',
|
||||
globalConfig: 'globalConfig/get',
|
||||
}),
|
||||
// Delete Modal
|
||||
deleteConfirmText() {
|
||||
return `${this.$t('INBOX_MGMT.DELETE.CONFIRM.YES')} ${
|
||||
this.selectedInbox.name
|
||||
}`;
|
||||
},
|
||||
deleteRejectText() {
|
||||
return `${this.$t('INBOX_MGMT.DELETE.CONFIRM.NO')} ${
|
||||
this.selectedInbox.name
|
||||
}`;
|
||||
},
|
||||
confirmDeleteMessage() {
|
||||
return `${this.$t('INBOX_MGMT.DELETE.CONFIRM.MESSAGE')} ${
|
||||
this.selectedInbox.name
|
||||
}?`;
|
||||
},
|
||||
confirmPlaceHolderText() {
|
||||
return `${this.$t('INBOX_MGMT.DELETE.CONFIRM.PLACE_HOLDER', {
|
||||
inboxName: this.selectedInbox.name,
|
||||
})}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
twilioChannelName(item) {
|
||||
const { medium = '' } = item;
|
||||
if (medium === 'whatsapp') return 'WhatsApp';
|
||||
return 'Twilio SMS';
|
||||
},
|
||||
openSettings(inbox) {
|
||||
this.showSettings = true;
|
||||
this.selectedInbox = inbox;
|
||||
},
|
||||
closeSettings() {
|
||||
this.showSettings = false;
|
||||
this.selectedInbox = {};
|
||||
},
|
||||
async deleteInbox({ id }) {
|
||||
try {
|
||||
await this.$store.dispatch('inboxes/delete', id);
|
||||
useAlert(this.$t('INBOX_MGMT.DELETE.API.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
useAlert(this.$t('INBOX_MGMT.DELETE.API.ERROR_MESSAGE'));
|
||||
}
|
||||
},
|
||||
const getters = useStoreGetters();
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
const { isAdmin } = useAdmin();
|
||||
|
||||
confirmDeletion() {
|
||||
this.deleteInbox(this.selectedInbox);
|
||||
this.closeDelete();
|
||||
},
|
||||
openDelete(inbox) {
|
||||
this.showDeletePopup = true;
|
||||
this.selectedInbox = inbox;
|
||||
},
|
||||
closeDelete() {
|
||||
this.showDeletePopup = false;
|
||||
this.selectedInbox = {};
|
||||
},
|
||||
},
|
||||
const showDeletePopup = ref(false);
|
||||
const selectedInbox = ref({});
|
||||
|
||||
const inboxesList = computed(() => getters['inboxes/getInboxes'].value);
|
||||
const uiFlags = computed(() => getters['inboxes/getUIFlags'].value);
|
||||
|
||||
const deleteConfirmText = computed(
|
||||
() => `${t('INBOX_MGMT.DELETE.CONFIRM.YES')} ${selectedInbox.value.name}`
|
||||
);
|
||||
|
||||
const deleteRejectText = computed(
|
||||
() => `${t('INBOX_MGMT.DELETE.CONFIRM.NO')} ${selectedInbox.value.name}`
|
||||
);
|
||||
|
||||
const confirmDeleteMessage = computed(
|
||||
() => `${t('INBOX_MGMT.DELETE.CONFIRM.MESSAGE')} ${selectedInbox.value.name}?`
|
||||
);
|
||||
const confirmPlaceHolderText = computed(
|
||||
() =>
|
||||
`${t('INBOX_MGMT.DELETE.CONFIRM.PLACE_HOLDER', {
|
||||
inboxName: selectedInbox.value.name,
|
||||
})}`
|
||||
);
|
||||
|
||||
const deleteInbox = async ({ id }) => {
|
||||
try {
|
||||
await store.dispatch('inboxes/delete', id);
|
||||
useAlert(t('INBOX_MGMT.DELETE.API.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
useAlert(t('INBOX_MGMT.DELETE.API.ERROR_MESSAGE'));
|
||||
}
|
||||
};
|
||||
const closeDelete = () => {
|
||||
showDeletePopup.value = false;
|
||||
selectedInbox.value = {};
|
||||
};
|
||||
|
||||
const confirmDeletion = () => {
|
||||
deleteInbox(selectedInbox.value);
|
||||
closeDelete();
|
||||
};
|
||||
const openDelete = inbox => {
|
||||
showDeletePopup.value = true;
|
||||
selectedInbox.value = inbox;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-1 overflow-auto">
|
||||
<div class="flex flex-row gap-4 p-8">
|
||||
<div class="w-full lg:w-3/5">
|
||||
<p
|
||||
v-if="!inboxesList.length"
|
||||
class="flex flex-col items-center justify-center h-full"
|
||||
>
|
||||
{{ $t('INBOX_MGMT.LIST.404') }}
|
||||
<SettingsLayout
|
||||
:no-records-found="!inboxesList.length"
|
||||
:no-records-message="$t('INBOX_MGMT.LIST.404')"
|
||||
:is-loading="uiFlags.isFetching"
|
||||
>
|
||||
<template #header>
|
||||
<BaseSettingsHeader
|
||||
:title="$t('INBOX_MGMT.HEADER')"
|
||||
:description="$t('INBOX_MGMT.DESCRIPTION')"
|
||||
:link-text="$t('INBOX_MGMT.LEARN_MORE')"
|
||||
feature-name="inboxes"
|
||||
>
|
||||
<template #actions>
|
||||
<router-link
|
||||
v-if="isAdmin"
|
||||
:to="accountScopedUrl('settings/inboxes/new')"
|
||||
class="button nice rounded-md"
|
||||
:to="{ name: 'settings_inbox_new' }"
|
||||
>
|
||||
<fluent-icon icon="add-circle" />
|
||||
{{ $t('SETTINGS.INBOXES.NEW_INBOX') }}
|
||||
</router-link>
|
||||
</p>
|
||||
|
||||
<table v-if="inboxesList.length" class="woot-table">
|
||||
<tbody>
|
||||
<tr v-for="item in inboxesList" :key="item.id">
|
||||
<td>
|
||||
<img
|
||||
v-if="item.avatar_url"
|
||||
class="woot-thumbnail"
|
||||
:src="item.avatar_url"
|
||||
alt="No Page Image"
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
<template #body>
|
||||
<table
|
||||
class="min-w-full overflow-x-auto divide-y divide-slate-75 dark:divide-slate-700"
|
||||
>
|
||||
<tbody
|
||||
class="divide-y divide-slate-25 dark:divide-slate-800 flex-1 text-slate-700 dark:text-slate-100"
|
||||
>
|
||||
<tr v-for="inbox in inboxesList" :key="inbox.id">
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4">
|
||||
<div class="flex items-center flex-row gap-4">
|
||||
<Thumbnail
|
||||
v-if="inbox.avatar_url"
|
||||
class="bg-black-50 dark:bg-black-800 rounded-full p-2 ring ring-opacity-20 dark:ring-opacity-80 ring-black-100 dark:ring-black-900 border border-slate-100 dark:border-slate-700/50 shadow-sm"
|
||||
:src="inbox.avatar_url"
|
||||
:username="inbox.name"
|
||||
size="48px"
|
||||
/>
|
||||
<img
|
||||
<div
|
||||
v-else
|
||||
class="woot-thumbnail"
|
||||
src="~dashboard/assets/images/flag.svg"
|
||||
alt="No Page Image"
|
||||
/>
|
||||
</td>
|
||||
<!-- Short Code -->
|
||||
<td>
|
||||
<span class="agent-name">{{ item.name }}</span>
|
||||
<span v-if="item.channel_type === 'Channel::FacebookPage'">
|
||||
{{ 'Facebook' }}
|
||||
</span>
|
||||
<span v-if="item.channel_type === 'Channel::WebWidget'">
|
||||
{{ 'Website' }}
|
||||
</span>
|
||||
<span v-if="item.channel_type === 'Channel::TwitterProfile'">
|
||||
{{ 'Twitter' }}
|
||||
</span>
|
||||
<span v-if="item.channel_type === 'Channel::TwilioSms'">
|
||||
{{ twilioChannelName(item) }}
|
||||
</span>
|
||||
<span v-if="item.channel_type === 'Channel::Whatsapp'">
|
||||
{{ 'Whatsapp' }}
|
||||
</span>
|
||||
<span v-if="item.channel_type === 'Channel::Sms'">
|
||||
{{ 'Sms' }}
|
||||
</span>
|
||||
<span v-if="item.channel_type === 'Channel::Email'">
|
||||
{{ 'Email' }}
|
||||
</span>
|
||||
<span v-if="item.channel_type === 'Channel::Telegram'">
|
||||
{{ 'Telegram' }}
|
||||
</span>
|
||||
<span v-if="item.channel_type === 'Channel::Line'">
|
||||
{{ 'Line' }}
|
||||
</span>
|
||||
<span v-if="item.channel_type === 'Channel::Api'">
|
||||
{{ globalConfig.apiChannelName || 'API' }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<td>
|
||||
<div class="button-wrapper">
|
||||
<router-link
|
||||
:to="accountScopedUrl(`settings/inboxes/${item.id}`)"
|
||||
class="w-12 h-12 bg-black-50 dark:bg-black-800 rounded-full p-2 ring ring-opacity-20 dark:ring-opacity-80 ring-black-100 dark:ring-black-900 border border-slate-100 dark:border-slate-700/50 shadow-sm block"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 24 24"
|
||||
class="opacity-80 p-1"
|
||||
>
|
||||
<woot-button
|
||||
v-if="isAdmin"
|
||||
v-tooltip.top="$t('INBOX_MGMT.SETTINGS')"
|
||||
variant="smooth"
|
||||
size="tiny"
|
||||
icon="settings"
|
||||
color-scheme="secondary"
|
||||
class-names="grey-btn"
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M1 12c0-5.185 0-7.778 1.61-9.39C4.223 1 6.816 1 12 1s7.778 0 9.39 1.61C23 4.223 23 6.816 23 12s0 7.778-1.61 9.39C19.777 23 17.184 23 12 23s-7.778 0-9.39-1.61C1 19.777 1 17.184 1 12"
|
||||
opacity=".35"
|
||||
/>
|
||||
</router-link>
|
||||
|
||||
<woot-button
|
||||
v-if="isAdmin"
|
||||
v-tooltip.top="$t('INBOX_MGMT.DELETE.BUTTON_TEXT')"
|
||||
variant="smooth"
|
||||
color-scheme="alert"
|
||||
size="tiny"
|
||||
class-names="grey-btn"
|
||||
:is-loading="loading[item.id]"
|
||||
icon="dismiss-circle"
|
||||
@click="openDelete(item)"
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M2.61 21.389c1.612 1.61 4.205 1.61 9.39 1.61s7.778 0 9.39-1.61c1.492-1.493 1.601-3.829 1.61-8.29h-3.476c-.996 0-1.494 0-1.931.202c-.438.201-.762.58-1.41 1.335l-.666.777c-.648.756-.972 1.134-1.41 1.335s-.935.202-1.93.202h-.353c-.996 0-1.494 0-1.931-.202c-.438-.2-.762-.579-1.41-1.335l-.666-.777c-.648-.756-.972-1.134-1.41-1.335s-.935-.201-1.93-.201H1c.008 4.46.118 6.796 1.61 8.289"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<span class="block font-medium capitalize">
|
||||
{{ inbox.name }}
|
||||
</span>
|
||||
<ChannelName
|
||||
:channel-type="inbox.channel_type"
|
||||
:medium="inbox.medium"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<div class="hidden w-1/3 lg:block">
|
||||
<span
|
||||
v-dompurify-html="
|
||||
useInstallationName(
|
||||
$t('INBOX_MGMT.SIDEBAR_TXT'),
|
||||
globalConfig.installationName
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Settings
|
||||
v-if="showSettings"
|
||||
:show.sync="showSettings"
|
||||
:on-close="closeSettings"
|
||||
:inbox="selectedInbox"
|
||||
/>
|
||||
<td class="py-4">
|
||||
<div class="flex gap-1 justify-end">
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'settings_inbox_show',
|
||||
params: { inboxId: inbox.id },
|
||||
}"
|
||||
>
|
||||
<woot-button
|
||||
v-if="isAdmin"
|
||||
v-tooltip.top="$t('INBOX_MGMT.SETTINGS')"
|
||||
variant="smooth"
|
||||
size="tiny"
|
||||
icon="settings"
|
||||
color-scheme="secondary"
|
||||
class-names="grey-btn"
|
||||
/>
|
||||
</router-link>
|
||||
|
||||
<woot-button
|
||||
v-if="isAdmin"
|
||||
v-tooltip.top="$t('INBOX_MGMT.DELETE.BUTTON_TEXT')"
|
||||
variant="smooth"
|
||||
color-scheme="alert"
|
||||
size="tiny"
|
||||
class-names="grey-btn"
|
||||
icon="dismiss-circle"
|
||||
@click="openDelete(inbox)"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<woot-confirm-delete-modal
|
||||
v-if="showDeletePopup"
|
||||
@@ -227,5 +185,5 @@ export default {
|
||||
@onConfirm="confirmDeletion"
|
||||
@onClose="closeDelete"
|
||||
/>
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<script setup>
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { useStoreGetters } from 'dashboard/composables/store';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
channelType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
medium: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
const getters = useStoreGetters();
|
||||
const { t } = useI18n();
|
||||
const globalConfig = computed(() => getters['globalConfig/get'].value);
|
||||
|
||||
const i18nMap = {
|
||||
'Channel::FacebookPage': 'MESSENGER',
|
||||
'Channel::WebWidget': 'WEB_WIDGET',
|
||||
'Channel::TwitterProfile': 'TWITTER_PROFILE',
|
||||
'Channel::TwilioSms': 'TWILIO_SMS',
|
||||
'Channel::Whatsapp': 'WHATSAPP',
|
||||
'Channel::Sms': 'SMS',
|
||||
'Channel::Email': 'EMAIL',
|
||||
'Channel::Telegram': 'TELEGRAM',
|
||||
'Channel::Line': 'LINE',
|
||||
'Channel::Api': 'API',
|
||||
};
|
||||
|
||||
const twilioChannelName = () => {
|
||||
if (props.medium === 'whatsapp') {
|
||||
return t(`INBOX_MGMT.CHANNELS.WHATSAPP`);
|
||||
}
|
||||
return t(`INBOX_MGMT.CHANNELS.TWILIO_SMS`);
|
||||
};
|
||||
|
||||
const readableChannelName = computed(() => {
|
||||
if (props.channelType === 'Channel::Api') {
|
||||
return globalConfig.value.apiChannelName || t('INBOX_MGMT.CHANNELS.API');
|
||||
}
|
||||
if (props.channelType === 'Channel::TwilioSms') {
|
||||
return twilioChannelName();
|
||||
}
|
||||
return t(`INBOX_MGMT.CHANNELS.${i18nMap[props.channelType]}`);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span>
|
||||
{{ readableChannelName }}
|
||||
</span>
|
||||
</template>
|
||||
@@ -1,8 +1,8 @@
|
||||
/* eslint arrow-body-style: 0 */
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
import channelFactory from './channel-factory';
|
||||
|
||||
const SettingsContent = () => import('../Wrapper.vue');
|
||||
const SettingWrapper = () => import('../SettingsWrapper.vue');
|
||||
const InboxHome = () => import('./Index.vue');
|
||||
const Settings = () => import('./Settings.vue');
|
||||
const InboxChannel = () => import('./InboxChannels.vue');
|
||||
@@ -12,6 +12,24 @@ const FinishSetup = () => import('./FinishSetup.vue');
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/settings/inboxes'),
|
||||
component: SettingWrapper,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
redirect: 'list',
|
||||
},
|
||||
{
|
||||
path: 'list',
|
||||
name: 'settings_inbox_list',
|
||||
component: InboxHome,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/settings/inboxes'),
|
||||
component: SettingsContent,
|
||||
@@ -26,18 +44,6 @@ export default {
|
||||
};
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
redirect: 'list',
|
||||
},
|
||||
{
|
||||
path: 'list',
|
||||
name: 'settings_inbox_list',
|
||||
component: InboxHome,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'new',
|
||||
component: InboxChannel,
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import Integration from './Integration.vue';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import SelectChannelWarning from './Slack/SelectChannelWarning.vue';
|
||||
import SlackIntegrationHelpText from './Slack/SlackIntegrationHelpText.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
@@ -13,7 +12,7 @@ export default {
|
||||
SelectChannelWarning,
|
||||
SlackIntegrationHelpText,
|
||||
},
|
||||
mixins: [globalConfigMixin, messageFormatterMixin],
|
||||
mixins: [globalConfigMixin],
|
||||
props: {
|
||||
code: { type: String, default: '' },
|
||||
},
|
||||
|
||||
+8
-2
@@ -2,16 +2,22 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
|
||||
export default {
|
||||
mixins: [globalConfigMixin, messageFormatterMixin],
|
||||
mixins: [globalConfigMixin],
|
||||
props: {
|
||||
hasConnectedAChannel: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
return {
|
||||
formatMessage,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return { selectedChannelId: '', availableChannels: [] };
|
||||
},
|
||||
|
||||
+7
-2
@@ -1,13 +1,18 @@
|
||||
<script>
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
export default {
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
selectedChannelName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
return {
|
||||
formatMessage,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
+60
-66
@@ -3,16 +3,18 @@ import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import NewWebhook from './NewWebHook.vue';
|
||||
import EditWebhook from './EditWebHook.vue';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import WebhookRow from './WebhookRow.vue';
|
||||
import BaseSettingsHeader from '../../components/BaseSettingsHeader.vue';
|
||||
import SettingsLayout from '../../SettingsLayout.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SettingsLayout,
|
||||
BaseSettingsHeader,
|
||||
NewWebhook,
|
||||
EditWebhook,
|
||||
WebhookRow,
|
||||
},
|
||||
mixins: [globalConfigMixin],
|
||||
data() {
|
||||
return {
|
||||
loading: {},
|
||||
@@ -26,8 +28,10 @@ export default {
|
||||
...mapGetters({
|
||||
records: 'webhooks/getWebhooks',
|
||||
uiFlags: 'webhooks/getUIFlags',
|
||||
globalConfig: 'globalConfig/get',
|
||||
}),
|
||||
integration() {
|
||||
return this.$store.getters['integrations/getIntegration']('webhook');
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('webhooks/get');
|
||||
@@ -75,69 +79,59 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-1 p-4 overflow-auto">
|
||||
<woot-button
|
||||
color-scheme="success"
|
||||
class-names="button--fixed-top"
|
||||
icon="add-circle"
|
||||
@click="openAddPopup"
|
||||
>
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.HEADER_BTN_TXT') }}
|
||||
</woot-button>
|
||||
|
||||
<div class="flex flex-row gap-4">
|
||||
<div class="w-full lg:w-3/5">
|
||||
<p
|
||||
v-if="!uiFlags.fetchingList && !records.length"
|
||||
class="flex flex-col items-center justify-center h-full"
|
||||
<SettingsLayout
|
||||
:is-loading="uiFlags.fetchingList"
|
||||
:loading-message="$t('INTEGRATION_SETTINGS.WEBHOOK.LOADING')"
|
||||
:no-records-message="$t('INTEGRATION_SETTINGS.WEBHOOK.LIST.404')"
|
||||
:no-records-found="!records.length"
|
||||
>
|
||||
<template #header>
|
||||
<BaseSettingsHeader
|
||||
v-if="integration.name"
|
||||
:title="integration.name"
|
||||
:description="integration.description"
|
||||
:link-text="$t('INTEGRATION_SETTINGS.WEBHOOK.LEARN_MORE')"
|
||||
feature-name="webhook"
|
||||
:back-button-label="$t('INTEGRATION_SETTINGS.HEADER')"
|
||||
>
|
||||
<template #actions>
|
||||
<woot-button
|
||||
class="button nice rounded-md"
|
||||
icon="add-circle"
|
||||
@click="openAddPopup"
|
||||
>
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.HEADER_BTN_TXT') }}
|
||||
</woot-button>
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
<template #body>
|
||||
<table class="min-w-full divide-y divide-slate-75 dark:divide-slate-700">
|
||||
<thead>
|
||||
<th
|
||||
v-for="thHeader in $t(
|
||||
'INTEGRATION_SETTINGS.WEBHOOK.LIST.TABLE_HEADER'
|
||||
)"
|
||||
:key="thHeader"
|
||||
class="py-4 pr-4 text-left font-semibold text-slate-700 dark:text-slate-300 last:text-right last:pr-4"
|
||||
>
|
||||
{{ thHeader }}
|
||||
</th>
|
||||
</thead>
|
||||
<tbody
|
||||
class="divide-y divide-slate-25 dark:divide-slate-800 flex-1 text-slate-700 dark:text-slate-100"
|
||||
>
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.LIST.404') }}
|
||||
</p>
|
||||
<woot-loading-state
|
||||
v-if="uiFlags.fetchingList"
|
||||
:message="$t('INTEGRATION_SETTINGS.WEBHOOK.LOADING')"
|
||||
/>
|
||||
|
||||
<table
|
||||
v-if="!uiFlags.fetchingList && records.length"
|
||||
class="woot-table"
|
||||
>
|
||||
<thead>
|
||||
<th
|
||||
v-for="thHeader in $t(
|
||||
'INTEGRATION_SETTINGS.WEBHOOK.LIST.TABLE_HEADER'
|
||||
)"
|
||||
:key="thHeader"
|
||||
class="last:text-right"
|
||||
>
|
||||
{{ thHeader }}
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<WebhookRow
|
||||
v-for="(webHookItem, index) in records"
|
||||
:key="webHookItem.id"
|
||||
:index="index"
|
||||
:webhook="webHookItem"
|
||||
@edit="openEditPopup"
|
||||
@delete="openDeletePopup"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="hidden w-1/3 lg:block">
|
||||
<span
|
||||
v-dompurify-html="
|
||||
useInstallationName(
|
||||
$t('INTEGRATION_SETTINGS.WEBHOOK.SIDEBAR_TXT'),
|
||||
globalConfig.installationName
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<WebhookRow
|
||||
v-for="(webHookItem, index) in records"
|
||||
:key="webHookItem.id"
|
||||
:index="index"
|
||||
:webhook="webHookItem"
|
||||
@edit="openEditPopup"
|
||||
@delete="openDeletePopup"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
<woot-modal :show.sync="showAddPopup" :on-close="hideAddPopup">
|
||||
<NewWebhook v-if="showAddPopup" :on-close="hideAddPopup" />
|
||||
</woot-modal>
|
||||
@@ -163,5 +157,5 @@ export default {
|
||||
:confirm-text="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.YES')"
|
||||
:reject-text="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.NO')"
|
||||
/>
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
|
||||
+4
-10
@@ -1,8 +1,8 @@
|
||||
<script>
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, url, minLength } from '@vuelidate/validators';
|
||||
import webhookMixin from './webhookMixin';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { getEventNamei18n } from './webhookHelper';
|
||||
|
||||
const { EXAMPLE_WEBHOOK_URL } = wootConstants;
|
||||
|
||||
@@ -18,7 +18,6 @@ const SUPPORTED_WEBHOOK_EVENTS = [
|
||||
];
|
||||
|
||||
export default {
|
||||
mixins: [webhookMixin],
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
@@ -70,6 +69,7 @@ export default {
|
||||
subscriptions: this.subscriptions,
|
||||
});
|
||||
},
|
||||
getEventNamei18n,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -105,10 +105,10 @@ export default {
|
||||
type="checkbox"
|
||||
:value="event"
|
||||
name="subscriptions"
|
||||
class="checkbox"
|
||||
class="mr-2"
|
||||
/>
|
||||
<label :for="event" class="text-sm">
|
||||
{{ `${getEventLabel(event)} (${event})` }}
|
||||
{{ `${$t(getEventNamei18n(event))} (${event})` }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -129,9 +129,3 @@ export default {
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.checkbox {
|
||||
@apply mr-2;
|
||||
}
|
||||
</style>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user