Compare commits
44
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4ede973e9 | ||
|
|
d79d9e8b46 | ||
|
|
b524ceeca1 | ||
|
|
e68dac8c45 | ||
|
|
f0d00b6965 | ||
|
|
f15a4f8aa4 | ||
|
|
c0d5b24881 | ||
|
|
377fb5c3b9 | ||
|
|
2ae545b394 | ||
|
|
58e78621ba | ||
|
|
42586e6b1e | ||
|
|
7baa7d8c62 | ||
|
|
82dec3cee9 | ||
|
|
fba73c7186 | ||
|
|
467700499b | ||
|
|
5d52e4e0a6 | ||
|
|
05b8486538 | ||
|
|
b9ff164041 | ||
|
|
73d448a203 | ||
|
|
8d8ec23aa1 | ||
|
|
add6403aa6 | ||
|
|
4b779dbe0f | ||
|
|
ab86f62fcc | ||
|
|
aaab2ac788 | ||
|
|
aa4edaa1e6 | ||
|
|
a76cd7684a | ||
|
|
2c17c95eab | ||
|
|
bb74c621b5 | ||
|
|
185a122ec7 | ||
|
|
05dece826d | ||
|
|
528b984a8d | ||
|
|
db0e654c03 | ||
|
|
e99e6a8443 | ||
|
|
d6fd3ce048 | ||
|
|
8a2f652b94 | ||
|
|
a3732c8f51 | ||
|
|
3a0e68030a | ||
|
|
f087461abc | ||
|
|
3a47b7e3d1 | ||
|
|
6dda1e8c8f | ||
|
|
098825c149 | ||
|
|
31e7663258 | ||
|
|
3b5f5b41ad | ||
|
|
fe5670832a |
@@ -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
|
||||
@@ -228,7 +228,7 @@ group :development, :test do
|
||||
gem 'mock_redis'
|
||||
gem 'pry-rails'
|
||||
gem 'rspec_junit_formatter'
|
||||
gem 'rspec-rails', '>= 6.1.4'
|
||||
gem 'rspec-rails', '>= 6.1.5'
|
||||
gem 'rubocop', require: false
|
||||
gem 'rubocop-performance', require: false
|
||||
gem 'rubocop-rails', require: false
|
||||
|
||||
+30
-30
@@ -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)
|
||||
google-protobuf (3.25.5)
|
||||
google-protobuf (3.25.5-arm64-darwin)
|
||||
google-protobuf (3.25.5-x86_64-darwin)
|
||||
google-protobuf (3.25.5-x86_64-linux)
|
||||
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)
|
||||
@@ -552,7 +552,7 @@ GEM
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (6.0.0)
|
||||
puma (6.4.2)
|
||||
puma (6.4.3)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.3.0)
|
||||
activesupport (>= 3.0.0)
|
||||
@@ -638,16 +638,16 @@ GEM
|
||||
strscan
|
||||
rspec-core (3.13.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-expectations (3.13.1)
|
||||
rspec-expectations (3.13.2)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-mocks (3.13.1)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-rails (6.1.4)
|
||||
actionpack (>= 6.1)
|
||||
activesupport (>= 6.1)
|
||||
railties (>= 6.1)
|
||||
rspec-rails (7.0.1)
|
||||
actionpack (>= 7.0)
|
||||
activesupport (>= 7.0)
|
||||
railties (>= 7.0)
|
||||
rspec-core (~> 3.13)
|
||||
rspec-expectations (~> 3.13)
|
||||
rspec-mocks (~> 3.13)
|
||||
@@ -819,7 +819,7 @@ GEM
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 5.2)
|
||||
semantic_range (>= 2.3.0)
|
||||
webrick (1.8.1)
|
||||
webrick (1.8.2)
|
||||
websocket-driver (0.7.6)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
@@ -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
|
||||
@@ -930,7 +930,7 @@ DEPENDENCIES
|
||||
responders (>= 3.1.1)
|
||||
rest-client
|
||||
reverse_markdown
|
||||
rspec-rails (>= 6.1.4)
|
||||
rspec-rails (>= 6.1.5)
|
||||
rspec_junit_formatter
|
||||
rubocop
|
||||
rubocop-performance
|
||||
|
||||
@@ -94,8 +94,8 @@ class AccountBuilder
|
||||
|
||||
def blocked_domains
|
||||
domains = GlobalConfigService.load('BLOCKED_EMAIL_DOMAINS', '')
|
||||
domains.split("\n").map(&:strip) if domains.present?
|
||||
return [] if domains.blank?
|
||||
|
||||
[]
|
||||
domains.split("\n").map(&:strip)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,7 +13,7 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
|
||||
|
||||
def destroy
|
||||
ActiveRecord::Base.transaction do
|
||||
message.update!(content: I18n.t('conversations.messages.deleted'), content_attributes: { deleted: true })
|
||||
message.update!(content: I18n.t('conversations.messages.deleted'), content_type: :text, content_attributes: { deleted: true })
|
||||
message.attachments.destroy_all
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,6 +11,8 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||
process_update_contact
|
||||
@conversation = create_conversation
|
||||
conversation.messages.create!(message_params)
|
||||
# TODO: Temporary fix for message type cast issue, since message_type is returning as string instead of integer
|
||||
conversation.reload
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -66,7 +66,9 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def check_authorization
|
||||
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
|
||||
return if Current.account_user.administrator?
|
||||
|
||||
raise Pundit::NotAuthorizedError
|
||||
end
|
||||
|
||||
def common_params
|
||||
@@ -135,3 +137,5 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||
V2::ReportBuilder.new(Current.account, conversation_params).conversation_metrics
|
||||
end
|
||||
end
|
||||
|
||||
Api::V2::Accounts::ReportsController.prepend_mod_with('Api::V2::Accounts::ReportsController')
|
||||
|
||||
@@ -6,7 +6,7 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
|
||||
layout 'portal'
|
||||
|
||||
def index
|
||||
@articles = @portal.articles
|
||||
@articles = @portal.articles.published
|
||||
search_articles
|
||||
order_by_sort_param
|
||||
@articles.page(list_params[:page]) if list_params[:page].present?
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class CustomRole extends ApiClient {
|
||||
constructor() {
|
||||
super('custom_roles', { accountScoped: true });
|
||||
}
|
||||
}
|
||||
|
||||
export default new CustomRole();
|
||||
@@ -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';
|
||||
@@ -27,6 +27,11 @@ import {
|
||||
} from '../store/modules/conversations/helpers/actionHelpers';
|
||||
import { CONVERSATION_EVENTS } from '../helper/AnalyticsHelper/events';
|
||||
import IntersectionObserver from './IntersectionObserver.vue';
|
||||
import {
|
||||
getUserPermissions,
|
||||
filterItemsByPermission,
|
||||
} from 'dashboard/helper/permissionsHelper.js';
|
||||
import { ASSIGNEE_TYPE_TAB_PERMISSIONS } from 'dashboard/constants/permissions.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -41,7 +46,6 @@ export default {
|
||||
IntersectionObserver,
|
||||
VirtualList,
|
||||
},
|
||||
mixins: [filterMixin],
|
||||
provide() {
|
||||
return {
|
||||
// Actions to be performed on virtual list item and context menu.
|
||||
@@ -91,6 +95,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 +122,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 +165,9 @@ export default {
|
||||
return {
|
||||
uiSettings,
|
||||
conversationListRef,
|
||||
setFilterAttributes,
|
||||
initializeStatusAndAssigneeFilterToModal,
|
||||
initializeInboxTeamAndLabelFilterToModal,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
@@ -187,6 +209,7 @@ export default {
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
currentAccountId: 'getCurrentAccountId',
|
||||
chatLists: 'getAllConversations',
|
||||
mineChatsList: 'getMineChats',
|
||||
allChatList: 'getAllStatusChats',
|
||||
@@ -226,20 +249,19 @@ export default {
|
||||
name,
|
||||
};
|
||||
},
|
||||
userPermissions() {
|
||||
return getUserPermissions(this.currentUser, this.currentAccountId);
|
||||
},
|
||||
assigneeTabItems() {
|
||||
const ASSIGNEE_TYPE_TAB_KEYS = {
|
||||
me: 'mineCount',
|
||||
unassigned: 'unAssignedCount',
|
||||
all: 'allCount',
|
||||
};
|
||||
return Object.keys(ASSIGNEE_TYPE_TAB_KEYS).map(key => {
|
||||
const count = this.conversationStats[ASSIGNEE_TYPE_TAB_KEYS[key]] || 0;
|
||||
return {
|
||||
key,
|
||||
name: this.$t(`CHAT_LIST.ASSIGNEE_TYPE_TABS.${key}`),
|
||||
count,
|
||||
};
|
||||
});
|
||||
return filterItemsByPermission(
|
||||
ASSIGNEE_TYPE_TAB_PERMISSIONS,
|
||||
this.userPermissions,
|
||||
item => item.permissions
|
||||
).map(({ key, count: countKey }) => ({
|
||||
key,
|
||||
name: this.$t(`CHAT_LIST.ASSIGNEE_TYPE_TABS.${key}`),
|
||||
count: this.conversationStats[countKey] || 0,
|
||||
}));
|
||||
},
|
||||
showAssigneeInConversationCard() {
|
||||
return (
|
||||
@@ -860,6 +882,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;
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ const settings = accountId => ({
|
||||
'settings_teams_list',
|
||||
'settings_teams_new',
|
||||
'sla_list',
|
||||
'custom_roles_list',
|
||||
],
|
||||
menuItems: [
|
||||
{
|
||||
@@ -178,6 +179,18 @@ const settings = accountId => ({
|
||||
isEnterpriseOnly: true,
|
||||
featureFlag: FEATURE_FLAGS.AUDIT_LOGS,
|
||||
},
|
||||
{
|
||||
icon: 'scan-person',
|
||||
label: 'CUSTOM_ROLES',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/custom-roles/list`),
|
||||
toStateName: 'custom_roles_list',
|
||||
isEnterpriseOnly: true,
|
||||
beta: true,
|
||||
},
|
||||
{
|
||||
icon: 'document-list-clock',
|
||||
label: 'SLA',
|
||||
|
||||
@@ -52,9 +52,13 @@ export default {
|
||||
{{ account.name }}
|
||||
</div>
|
||||
<div
|
||||
class="text-xs font-medium text-slate-500 dark:text-slate-500 hover:underline-offset-4"
|
||||
class="text-xs font-medium lowercase text-slate-500 dark:text-slate-500 hover:underline-offset-4"
|
||||
>
|
||||
{{ account.role }}
|
||||
{{
|
||||
account.custom_role_id
|
||||
? account.custom_role.name
|
||||
: account.role
|
||||
}}
|
||||
</div>
|
||||
</label>
|
||||
</span>
|
||||
|
||||
@@ -21,8 +21,11 @@ const activeTabIndex = computed(() => {
|
||||
});
|
||||
|
||||
const onTabChange = selectedTabIndex => {
|
||||
if (props.items[selectedTabIndex].key !== props.activeTab) {
|
||||
emit('chatTabChange', props.items[selectedTabIndex].key);
|
||||
if (selectedTabIndex >= 0 && selectedTabIndex < props.items.length) {
|
||||
const selectedItem = props.items[selectedTabIndex];
|
||||
if (selectedItem.key !== props.activeTab) {
|
||||
emit('chatTabChange', selectedItem.key);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -32,7 +35,8 @@ const keyboardEvents = {
|
||||
if (props.activeTab === wootConstants.ASSIGNEE_TYPE.ALL) {
|
||||
onTabChange(0);
|
||||
} else {
|
||||
onTabChange(activeTabIndex.value + 1);
|
||||
const nextIndex = (activeTabIndex.value + 1) % props.items.length;
|
||||
onTabChange(nextIndex);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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: '' },
|
||||
@@ -91,6 +97,7 @@ export default {
|
||||
allowSignature: { type: Boolean, default: false },
|
||||
channelType: { type: String, default: '' },
|
||||
showImageResizeToolbar: { type: Boolean, default: false }, // A kill switch to show or hide the image toolbar
|
||||
focusOnMount: { type: Boolean, default: true },
|
||||
},
|
||||
setup() {
|
||||
const {
|
||||
@@ -119,9 +126,11 @@ export default {
|
||||
showUserMentions: false,
|
||||
showCannedMenu: false,
|
||||
showVariables: false,
|
||||
showEmojiMenu: false,
|
||||
mentionSearchKey: '',
|
||||
cannedSearchTerm: '',
|
||||
variableSearchTerm: '',
|
||||
emojiSearchTerm: '',
|
||||
editorView: null,
|
||||
range: null,
|
||||
state: undefined,
|
||||
@@ -169,7 +178,7 @@ export default {
|
||||
this.editorView = args.view;
|
||||
this.range = args.range;
|
||||
|
||||
this.mentionSearchKey = args.text.replace('@', '');
|
||||
this.mentionSearchKey = args.text;
|
||||
|
||||
return false;
|
||||
},
|
||||
@@ -198,7 +207,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 +235,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 +247,31 @@ export default {
|
||||
return event.keyCode === 13 && this.showVariables;
|
||||
},
|
||||
}),
|
||||
suggestionsPlugin({
|
||||
matcher: triggerCharacters(':', 2), // Trigger after ':' and at least 2 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 +301,8 @@ export default {
|
||||
},
|
||||
editorId() {
|
||||
this.showCannedMenu = false;
|
||||
this.showEmojiMenu = false;
|
||||
this.showVariables = false;
|
||||
this.cannedSearchTerm = '';
|
||||
this.reloadState(this.value);
|
||||
},
|
||||
@@ -311,7 +347,9 @@ export default {
|
||||
mounted() {
|
||||
this.createEditorView();
|
||||
this.editorView.updateState(this.state);
|
||||
this.focusEditorInputField();
|
||||
if (this.focusOnMount) {
|
||||
this.focusEditorInputField();
|
||||
}
|
||||
|
||||
// BUS Event to insert text or markdown into the editor at the
|
||||
// current cursor position.
|
||||
@@ -348,7 +386,7 @@ export default {
|
||||
// these drafts can also have a signature, so we need to check if the body is empty
|
||||
// and handle things accordingly
|
||||
this.handleEmptyBodyWithSignature();
|
||||
} else {
|
||||
} else if (this.focusOnMount) {
|
||||
// this is in the else block, handleEmptyBodyWithSignature also has a call to the focus method
|
||||
// the position is set to start, because the signature is added at the end of the body
|
||||
this.focusEditorInputField('end');
|
||||
@@ -517,57 +555,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 +704,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();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -220,11 +220,7 @@ export default {
|
||||
: this.$t('CONVERSATION.FOOTER.ENABLE_SIGN_TOOLTIP');
|
||||
},
|
||||
enableInsertArticleInReply() {
|
||||
const isFeatEnabled = this.isFeatureEnabledonAccount(
|
||||
this.accountId,
|
||||
FEATURE_FLAGS.INSERT_ARTICLE_IN_REPLY
|
||||
);
|
||||
return isFeatEnabled && this.portalSlug;
|
||||
return this.portalSlug;
|
||||
},
|
||||
isFetchingAppIntegrations() {
|
||||
return this.uiFlags.isFetching;
|
||||
@@ -403,6 +399,7 @@ export default {
|
||||
label {
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
|
||||
&:hover button {
|
||||
@apply dark:bg-slate-800 bg-slate-100;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<script setup>
|
||||
import { shallowRef, computed, onMounted } from 'vue';
|
||||
import emojiGroups 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 = emojiGroups.flatMap(({ emojis }) =>
|
||||
emojis.map(({ name, slug, ...rest }) => ({
|
||||
...rest,
|
||||
name,
|
||||
slug,
|
||||
searchString: `${name.replace(/\s+/g, '')} ${slug}`.toLowerCase(), // Remove all whitespace and convert to lowercase
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
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.name }}
|
||||
</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)"
|
||||
/>
|
||||
|
||||
@@ -237,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,6 +1,10 @@
|
||||
<script>
|
||||
import countries from 'shared/constants/countries.js';
|
||||
import parsePhoneNumber from 'libphonenumber-js';
|
||||
import {
|
||||
getActiveCountryCode,
|
||||
getActiveDialCode,
|
||||
} from 'shared/components/PhoneInput/helper';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@@ -30,8 +34,8 @@ export default {
|
||||
selectedIndex: -1,
|
||||
showDropdown: false,
|
||||
searchCountry: '',
|
||||
activeCountryCode: '',
|
||||
activeDialCode: '',
|
||||
activeCountryCode: getActiveCountryCode(),
|
||||
activeDialCode: getActiveDialCode(),
|
||||
phoneNumber: this.value,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -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,53 @@
|
||||
export const AVAILABLE_CUSTOM_ROLE_PERMISSIONS = [
|
||||
'conversation_manage',
|
||||
'conversation_unassigned_manage',
|
||||
'conversation_participating_manage',
|
||||
'contact_manage',
|
||||
'report_manage',
|
||||
'knowledge_base_manage',
|
||||
];
|
||||
|
||||
export const ROLES = ['agent', 'administrator'];
|
||||
|
||||
export const CONVERSATION_PERMISSIONS = [
|
||||
'conversation_manage',
|
||||
'conversation_unassigned_manage',
|
||||
'conversation_participating_manage',
|
||||
];
|
||||
|
||||
export const MANAGE_ALL_CONVERSATION_PERMISSIONS = 'conversation_manage';
|
||||
|
||||
export const CONVERSATION_UNASSIGNED_PERMISSIONS =
|
||||
'conversation_unassigned_manage';
|
||||
|
||||
export const CONVERSATION_PARTICIPATING_PERMISSIONS =
|
||||
'conversation_participating_manage';
|
||||
|
||||
export const CONTACT_PERMISSIONS = 'contact_manage';
|
||||
|
||||
export const REPORTS_PERMISSIONS = 'report_manage';
|
||||
|
||||
export const PORTAL_PERMISSIONS = 'knowledge_base_manage';
|
||||
|
||||
export const ASSIGNEE_TYPE_TAB_PERMISSIONS = {
|
||||
me: {
|
||||
count: 'mineCount',
|
||||
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
|
||||
},
|
||||
unassigned: {
|
||||
count: 'unAssignedCount',
|
||||
permissions: [
|
||||
...ROLES,
|
||||
MANAGE_ALL_CONVERSATION_PERMISSIONS,
|
||||
CONVERSATION_UNASSIGNED_PERMISSIONS,
|
||||
],
|
||||
},
|
||||
all: {
|
||||
count: 'allCount',
|
||||
permissions: [
|
||||
...ROLES,
|
||||
MANAGE_ALL_CONVERSATION_PERMISSIONS,
|
||||
CONVERSATION_PARTICIPATING_PERMISSIONS,
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -16,7 +16,6 @@ export const FEATURE_FLAGS = {
|
||||
TEAM_MANAGEMENT: 'team_management',
|
||||
VOICE_RECORDER: 'voice_recorder',
|
||||
AUDIT_LOGS: 'audit_logs',
|
||||
INSERT_ARTICLE_IN_REPLY: 'insert_article_in_reply',
|
||||
INBOX_VIEW: 'inbox_view',
|
||||
SLA: 'sla',
|
||||
RESPONSE_BOT: 'response_bot',
|
||||
@@ -32,4 +31,5 @@ export const FEATURE_FLAGS = {
|
||||
IP_LOOKUP: 'ip_lookup',
|
||||
LINEAR: 'linear_integration',
|
||||
CAPTAIN: 'captain_integration',
|
||||
CUSTOM_ROLES: 'custom_roles',
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -5,6 +5,11 @@ import {
|
||||
getAlertAudio,
|
||||
initOnEvents,
|
||||
} from 'shared/helpers/AudioNotificationHelper';
|
||||
import {
|
||||
ROLES,
|
||||
CONVERSATION_PERMISSIONS,
|
||||
} from 'dashboard/constants/permissions.js';
|
||||
import { getUserPermissions } from 'dashboard/helper/permissionsHelper.js';
|
||||
|
||||
const NOTIFICATION_TIME = 30000;
|
||||
|
||||
@@ -14,12 +19,13 @@ class DashboardAudioNotificationHelper {
|
||||
this.audioAlertType = 'none';
|
||||
this.playAlertOnlyWhenHidden = true;
|
||||
this.alertIfUnreadConversationExist = false;
|
||||
this.currentUser = null;
|
||||
this.currentUserId = null;
|
||||
this.audioAlertTone = 'ding';
|
||||
}
|
||||
|
||||
setInstanceValues = ({
|
||||
currentUserId,
|
||||
currentUser,
|
||||
alwaysPlayAudioAlert,
|
||||
alertIfUnreadConversationExist,
|
||||
audioAlertType,
|
||||
@@ -28,7 +34,8 @@ class DashboardAudioNotificationHelper {
|
||||
this.audioAlertType = audioAlertType;
|
||||
this.playAlertOnlyWhenHidden = !alwaysPlayAudioAlert;
|
||||
this.alertIfUnreadConversationExist = alertIfUnreadConversationExist;
|
||||
this.currentUserId = currentUserId;
|
||||
this.currentUser = currentUser;
|
||||
this.currentUserId = currentUser.id;
|
||||
this.audioAlertTone = audioAlertTone;
|
||||
initOnEvents.forEach(e => {
|
||||
document.addEventListener(e, this.onAudioListenEvent, false);
|
||||
@@ -112,6 +119,20 @@ class DashboardAudioNotificationHelper {
|
||||
return message?.sender_id === this.currentUserId;
|
||||
};
|
||||
|
||||
isUserHasConversationPermission = () => {
|
||||
const currentAccountId = window.WOOT.$store.getters.getCurrentAccountId;
|
||||
// Get the user permissions for the current account
|
||||
const userPermissions = getUserPermissions(
|
||||
this.currentUser,
|
||||
currentAccountId
|
||||
);
|
||||
// Check if the user has the required permissions
|
||||
const hasRequiredPermission = [...ROLES, ...CONVERSATION_PERMISSIONS].some(
|
||||
permission => userPermissions.includes(permission)
|
||||
);
|
||||
return hasRequiredPermission;
|
||||
};
|
||||
|
||||
shouldNotifyOnMessage = message => {
|
||||
if (this.audioAlertType === 'mine') {
|
||||
return this.isConversationAssignedToCurrentUser(message);
|
||||
@@ -120,6 +141,11 @@ class DashboardAudioNotificationHelper {
|
||||
};
|
||||
|
||||
onNewMessage = message => {
|
||||
// If the user does not have the permission to view the conversation, then dismiss the alert
|
||||
if (!this.isUserHasConversationPermission()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the message is sent by the current user or the
|
||||
// correct notification is not enabled, then dismiss the alert
|
||||
if (
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -41,3 +41,32 @@ export const buildPermissionsFromRouter = (routes = []) =>
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
/**
|
||||
* Filters and transforms items based on user permissions.
|
||||
*
|
||||
* @param {Object} items - An object containing items to be filtered.
|
||||
* @param {Array} userPermissions - Array of permissions the user has.
|
||||
* @param {Function} getPermissions - Function to extract required permissions from an item.
|
||||
* @param {Function} [transformItem] - Optional function to transform each item after filtering.
|
||||
* @returns {Array} Filtered and transformed items.
|
||||
*/
|
||||
export const filterItemsByPermission = (
|
||||
items,
|
||||
userPermissions,
|
||||
getPermissions,
|
||||
transformItem = (key, item) => ({ key, ...item })
|
||||
) => {
|
||||
// Helper function to check if an item has the required permissions
|
||||
const hasRequiredPermissions = item => {
|
||||
const requiredPermissions = getPermissions(item);
|
||||
return (
|
||||
requiredPermissions.length === 0 ||
|
||||
hasPermissions(requiredPermissions, userPermissions)
|
||||
);
|
||||
};
|
||||
|
||||
return Object.entries(items)
|
||||
.filter(([, item]) => hasRequiredPermissions(item)) // Keep only items with required permissions
|
||||
.map(([key, item]) => transformItem(key, item)); // Transform each remaining item
|
||||
};
|
||||
|
||||
@@ -4,11 +4,39 @@ import {
|
||||
getCurrentAccount,
|
||||
} from './permissionsHelper';
|
||||
|
||||
import {
|
||||
ROLES,
|
||||
CONVERSATION_PERMISSIONS,
|
||||
CONTACT_PERMISSIONS,
|
||||
REPORTS_PERMISSIONS,
|
||||
PORTAL_PERMISSIONS,
|
||||
} from 'dashboard/constants/permissions.js';
|
||||
|
||||
export const routeIsAccessibleFor = (route, userPermissions = []) => {
|
||||
const { meta: { permissions: routePermissions = [] } = {} } = route;
|
||||
return hasPermissions(routePermissions, userPermissions);
|
||||
};
|
||||
|
||||
export const defaultRedirectPage = (to, permissions) => {
|
||||
const { accountId } = to.params;
|
||||
|
||||
const permissionRoutes = [
|
||||
{
|
||||
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
|
||||
path: 'dashboard',
|
||||
},
|
||||
{ permissions: [CONTACT_PERMISSIONS], path: 'contacts' },
|
||||
{ permissions: [REPORTS_PERMISSIONS], path: 'reports/overview' },
|
||||
{ permissions: [PORTAL_PERMISSIONS], path: 'portals' },
|
||||
];
|
||||
|
||||
const route = permissionRoutes.find(({ permissions: routePermissions }) =>
|
||||
hasPermissions(routePermissions, permissions)
|
||||
);
|
||||
|
||||
return `accounts/${accountId}/${route ? route.path : 'dashboard'}`;
|
||||
};
|
||||
|
||||
const validateActiveAccountRoutes = (to, user) => {
|
||||
// If the current account is active, then check for the route permissions
|
||||
const accountDashboardURL = `accounts/${to.params.accountId}/dashboard`;
|
||||
@@ -22,7 +50,7 @@ const validateActiveAccountRoutes = (to, user) => {
|
||||
|
||||
const isAccessible = routeIsAccessibleFor(to, userPermissions);
|
||||
// If the route is not accessible for the user, return to dashboard screen
|
||||
return isAccessible ? null : accountDashboardURL;
|
||||
return isAccessible ? null : defaultRedirectPage(to, userPermissions);
|
||||
};
|
||||
|
||||
export const validateLoggedInRoutes = (to, user) => {
|
||||
|
||||
@@ -26,7 +26,7 @@ const initializeAudioAlerts = user => {
|
||||
} = uiSettings || {};
|
||||
|
||||
DashboardAudioNotificationHelper.setInstanceValues({
|
||||
currentUserId: user.id,
|
||||
currentUser: user,
|
||||
audioAlertType: audioAlertType || 'none',
|
||||
audioAlertTone: audioAlertTone || 'ding',
|
||||
alwaysPlayAudioAlert: alwaysPlayAudioAlert || 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
getCurrentAccount,
|
||||
getUserPermissions,
|
||||
hasPermissions,
|
||||
filterItemsByPermission,
|
||||
} from '../permissionsHelper';
|
||||
|
||||
describe('#getCurrentAccount', () => {
|
||||
@@ -105,3 +106,113 @@ describe('buildPermissionsFromRouter', () => {
|
||||
}).toThrow("The route doesn't have the required permissions defined");
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterItemsByPermission', () => {
|
||||
const items = {
|
||||
item1: { name: 'Item 1', permissions: ['agent', 'administrator'] },
|
||||
item2: {
|
||||
name: 'Item 2',
|
||||
permissions: [
|
||||
'conversation_manage',
|
||||
'conversation_unassigned_manage',
|
||||
'conversation_participating_manage',
|
||||
],
|
||||
},
|
||||
item3: { name: 'Item 3', permissions: ['contact_manage'] },
|
||||
item4: { name: 'Item 4', permissions: ['report_manage'] },
|
||||
item5: { name: 'Item 5', permissions: ['knowledge_base_manage'] },
|
||||
item6: {
|
||||
name: 'Item 6',
|
||||
permissions: [
|
||||
'agent',
|
||||
'administrator',
|
||||
'conversation_manage',
|
||||
'conversation_unassigned_manage',
|
||||
'conversation_participating_manage',
|
||||
'contact_manage',
|
||||
'report_manage',
|
||||
'knowledge_base_manage',
|
||||
],
|
||||
},
|
||||
item7: { name: 'Item 7', permissions: [] },
|
||||
};
|
||||
|
||||
const getPermissions = item => item.permissions;
|
||||
|
||||
it('filters items based on user permissions', () => {
|
||||
const userPermissions = ['agent', 'contact_manage', 'report_manage'];
|
||||
const result = filterItemsByPermission(
|
||||
items,
|
||||
userPermissions,
|
||||
getPermissions
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(5);
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({ key: 'item1', name: 'Item 1' })
|
||||
);
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({ key: 'item3', name: 'Item 3' })
|
||||
);
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({ key: 'item4', name: 'Item 4' })
|
||||
);
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({ key: 'item6', name: 'Item 6' })
|
||||
);
|
||||
});
|
||||
|
||||
it('includes items with empty permissions', () => {
|
||||
const userPermissions = [];
|
||||
const result = filterItemsByPermission(
|
||||
items,
|
||||
userPermissions,
|
||||
getPermissions
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({ key: 'item7', name: 'Item 7' })
|
||||
);
|
||||
});
|
||||
|
||||
it('uses custom transform function when provided', () => {
|
||||
const userPermissions = ['agent', 'contact_manage'];
|
||||
const customTransform = (key, item) => ({ id: key, title: item.name });
|
||||
const result = filterItemsByPermission(
|
||||
items,
|
||||
userPermissions,
|
||||
getPermissions,
|
||||
customTransform
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(4);
|
||||
expect(result).toContainEqual({ id: 'item1', title: 'Item 1' });
|
||||
expect(result).toContainEqual({ id: 'item3', title: 'Item 3' });
|
||||
expect(result).toContainEqual({ id: 'item6', title: 'Item 6' });
|
||||
});
|
||||
|
||||
it('handles empty items object', () => {
|
||||
const result = filterItemsByPermission({}, ['agent'], getPermissions);
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('handles custom getPermissions function', () => {
|
||||
const customItems = {
|
||||
item1: { name: 'Item 1', requiredPerms: ['agent', 'administrator'] },
|
||||
item2: { name: 'Item 2', requiredPerms: ['contact_manage'] },
|
||||
};
|
||||
const customGetPermissions = item => item.requiredPerms;
|
||||
const result = filterItemsByPermission(
|
||||
customItems,
|
||||
['agent'],
|
||||
customGetPermissions
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({ key: 'item1', name: 'Item 1' })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
getConversationDashboardRoute,
|
||||
isAConversationRoute,
|
||||
defaultRedirectPage,
|
||||
routeIsAccessibleFor,
|
||||
validateLoggedInRoutes,
|
||||
isAInboxViewRoute,
|
||||
@@ -14,6 +15,57 @@ describe('#routeIsAccessibleFor', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#defaultRedirectPage', () => {
|
||||
const to = {
|
||||
params: { accountId: '2' },
|
||||
fullPath: '/app/accounts/2/dashboard',
|
||||
name: 'home',
|
||||
};
|
||||
|
||||
it('should return dashboard route for users with conversation permissions', () => {
|
||||
const permissions = ['conversation_manage', 'agent'];
|
||||
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/dashboard');
|
||||
});
|
||||
|
||||
it('should return contacts route for users with contact permissions', () => {
|
||||
const permissions = ['contact_manage'];
|
||||
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/contacts');
|
||||
});
|
||||
|
||||
it('should return reports route for users with report permissions', () => {
|
||||
const permissions = ['report_manage'];
|
||||
expect(defaultRedirectPage(to, permissions)).toBe(
|
||||
'accounts/2/reports/overview'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return portals route for users with portal permissions', () => {
|
||||
const permissions = ['knowledge_base_manage'];
|
||||
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/portals');
|
||||
});
|
||||
|
||||
it('should return dashboard route as default for users with custom roles', () => {
|
||||
const permissions = ['custom_role'];
|
||||
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/dashboard');
|
||||
});
|
||||
|
||||
it('should return dashboard route for users with administrator role', () => {
|
||||
const permissions = ['administrator'];
|
||||
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/dashboard');
|
||||
});
|
||||
|
||||
it('should return dashboard route for users with multiple permissions', () => {
|
||||
const permissions = [
|
||||
'contact_manage',
|
||||
'custom_role',
|
||||
'conversation_manage',
|
||||
'agent',
|
||||
'administrator',
|
||||
];
|
||||
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/dashboard');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#validateLoggedInRoutes', () => {
|
||||
describe('when account access is missing', () => {
|
||||
it('should return the login route', () => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
"STATUS": "Status",
|
||||
"ACTIONS": "Actions",
|
||||
"VERIFIED": "Verified",
|
||||
"VERIFICATION_PENDING": "Verification Pending"
|
||||
"VERIFICATION_PENDING": "Verification Pending",
|
||||
"AVAILABLE_CUSTOM_ROLE": "Available custom role permissions"
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Add agent to your team",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"AUTOMATION": {
|
||||
"HEADER": "Automations",
|
||||
"HEADER": "Automation",
|
||||
"DESCRIPTION": "Automation can replace and streamline existing processes that require manual effort, such as adding labels and assigning conversations to the most suitable agent. This allows the team to focus on their strengths while reducing time spent on routine tasks.",
|
||||
"LEARN_MORE": "Learn more about automation",
|
||||
"HEADER_BTN_TXT": "Add Automation Rule",
|
||||
"LOADING": "Fetching automation rules",
|
||||
"SIDEBAR_TXT": "<p><b>Automation Rules</b> <p>Automation can replace and automate existing processes that require manual effort. You can do many things with automation, including adding labels and assigning conversation to the best agent. So the team focuses on what they do best and spends more little time on manual tasks.</p>",
|
||||
"ADD": {
|
||||
"TITLE": "Add Automation Rule",
|
||||
"SUBMIT": "Create",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"CUSTOM_ROLE": {
|
||||
"HEADER": "Custom Roles",
|
||||
"LEARN_MORE": "Learn more about custom roles",
|
||||
"DESCRIPTION": "Custom roles are roles that are created by the account owner or admin. These roles can be assigned to agents to define their access and permissions within the account. Custom roles can be created with specific permissions and access levels to suit the requirements of the organization.",
|
||||
"HEADER_BTN_TXT": "Add custom role",
|
||||
"LOADING": "Fetching custom roles...",
|
||||
"SEARCH_404": "There are no items matching this query.",
|
||||
"PAYWALL": {
|
||||
"TITLE": "Upgrade to create custom roles",
|
||||
"AVAILABLE_ON": "The custom role feature is only available in the Business and Enterprise plans.",
|
||||
"UPGRADE_PROMPT": "Upgrade your plan to get access to advanced features like team management, automations, custom attributes, and more.",
|
||||
"UPGRADE_NOW": "Upgrade now",
|
||||
"CANCEL_ANYTIME": "You can change or cancel your plan anytime"
|
||||
},
|
||||
"ENTERPRISE_PAYWALL": {
|
||||
"AVAILABLE_ON": "The custom role feature is only available in the paid plans.",
|
||||
"UPGRADE_PROMPT": "Upgrade to a paid plan to access advanced features like audit logs, agent capacity, and more.",
|
||||
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
|
||||
},
|
||||
"LIST": {
|
||||
"404": "There are no custom roles available in this account.",
|
||||
"TITLE": "Manage custom roles",
|
||||
"DESC": "Custom roles are roles that are created by the account owner or admin. These roles can be assigned to agents to define their access and permissions within the account. Custom roles can be created with specific permissions and access levels to suit the requirements of the organization.",
|
||||
"TABLE_HEADER": [
|
||||
"Name",
|
||||
"Description",
|
||||
"Permissions",
|
||||
"Actions"
|
||||
]
|
||||
},
|
||||
"PERMISSIONS": {
|
||||
"CONVERSATION_MANAGE": "Manage all conversations",
|
||||
"CONVERSATION_UNASSIGNED_MANAGE": "Manage unassigned conversations and those assigned to them",
|
||||
"CONVERSATION_PARTICIPATING_MANAGE": "Manage participating conversations and those assigned to them",
|
||||
"CONTACT_MANAGE": "Manage contacts",
|
||||
"REPORT_MANAGE": "Manage reports",
|
||||
"KNOWLEDGE_BASE_MANAGE": "Manage knowledge base"
|
||||
},
|
||||
"FORM": {
|
||||
"NAME": {
|
||||
"LABEL": "Name",
|
||||
"PLACEHOLDER": "Please enter a name.",
|
||||
"ERROR": "Name is required."
|
||||
},
|
||||
"DESCRIPTION": {
|
||||
"LABEL": "Description",
|
||||
"PLACEHOLDER": "Please enter a description.",
|
||||
"ERROR": "Description is required."
|
||||
},
|
||||
"PERMISSIONS": {
|
||||
"LABEL": "Permissions",
|
||||
"ERROR": "Permissions are required."
|
||||
},
|
||||
"CANCEL_BUTTON_TEXT": "Cancel",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
|
||||
}
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Add custom role",
|
||||
"DESC": " Custom roles allows you to create roles with specific permissions and access levels to suit the requirements of the organization.",
|
||||
"SUBMIT": "Submit",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Custom role added successfully."
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
"BUTTON_TEXT": "Edit",
|
||||
"TITLE": "Edit custom role",
|
||||
"DESC": " Custom roles allows you to create roles with specific permissions and access levels to suit the requirements of the organization.",
|
||||
"SUBMIT": "Update",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Custom role updated successfully."
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
"BUTTON_TEXT": "Delete",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Custom role deleted successfully.",
|
||||
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
|
||||
},
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirm deletion",
|
||||
"MESSAGE": "Are you sure to delete ",
|
||||
"YES": "Yes, delete ",
|
||||
"NO": "No, keep "
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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": {
|
||||
@@ -367,12 +368,19 @@
|
||||
"DESCRIPTION": "Click on the Sign in with Microsoft button to get started. You will redirected to the email sign in page. Once you accept the requested permissions, you would be redirected back to the inbox creation step.",
|
||||
"EMAIL_PLACEHOLDER": "Enter email address",
|
||||
"SIGN_IN": "Sign in with Microsoft",
|
||||
"HELP": "To add your Microsoft account as a channel, you need to authenticate your Microsoft account by clicking on 'Sign in with Microsoft' ",
|
||||
"ERROR_MESSAGE": "There was an error connecting to Microsoft, please try again"
|
||||
},
|
||||
"GOOGLE": {
|
||||
"TITLE": "Google Email",
|
||||
"DESCRIPTION": "Click on the Sign in with Google button to get started. You will redirected to the email sign in page. Once you accept the requested permissions, you would be redirected back to the inbox creation step.",
|
||||
"SIGN_IN": "Sign in with Google",
|
||||
"EMAIL_PLACEHOLDER": "Enter email address",
|
||||
"ERROR_MESSAGE": "There was an error connecting to Google, please try again"
|
||||
}
|
||||
},
|
||||
"DETAILS": {
|
||||
"LOADING_FB": "Authenticating you with Facebook...",
|
||||
"ERROR_FB_LOADING": "Error loading Facebook SDK. Please disable any ad-blockers and try again from a different browser.",
|
||||
"ERROR_FB_AUTH": "Something went wrong, Please refresh page...",
|
||||
"ERROR_FB_UNAUTHORIZED": "You're not authorized to perform this action. ",
|
||||
"ERROR_FB_UNAUTHORIZED_HELP": "Please ensure you have access to the Facebook page with full control. You can read more about Facebook roles <a href=\" https://www.facebook.com/help/187316341316631\">here</a>.",
|
||||
@@ -736,7 +744,20 @@
|
||||
},
|
||||
"EMAIL_PROVIDERS": {
|
||||
"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,10 +1,11 @@
|
||||
{
|
||||
"MACROS": {
|
||||
"HEADER": "Macros",
|
||||
"DESCRIPTION": "A macro is a set of saved actions that help customer service agents easily complete tasks. The agents can define a set of actions like tagging a conversation with a label, sending an email transcript, updating a custom attribute, etc., and they can run these actions in a single click.",
|
||||
"LEARN_MORE": "Learn more about macros",
|
||||
"HEADER_BTN_TXT": "Add a new macro",
|
||||
"HEADER_BTN_TXT_SAVE": "Save macro",
|
||||
"LOADING": "Fetching macros",
|
||||
"SIDEBAR_TXT": "<p><b>Macros</b><p>A macro is a set of saved actions that help customer service agents easily complete tasks. The agents can define a set of actions like tagging a conversation with a label, sending an email transcript, updating a custom attribute, etc., and they can run these actions in a single click. When the agents run the macro, the actions would be performed sequentially in the order they are defined. Macros improve productivity and increase consistency in actions. </p><p>A macro can be helpful in 2 ways. </p><p><b>As an agent assist:</b> If an agent performs a set of actions multiple times, they can save it as a macro and execute all the actions together using a single click.</p><p><b>As an option to onboard a team member:</b> Every agent has to perform many different checks/actions during each conversation. Onboarding a new support team member will be easy if pre-defined macros are available on the account. Instead of describing each step in detail, the manager/team lead can point to the macros used in different scenarios.</p>",
|
||||
"ERROR": "Something went wrong. Please try again",
|
||||
"ORDER_INFO": "Macros will run in the order you add your actions. You can rearrange them by dragging them by the handle beside each node.",
|
||||
"ADD": {
|
||||
|
||||
@@ -277,6 +277,7 @@
|
||||
"REPORTS_TEAM": "Team",
|
||||
"SET_AVAILABILITY_TITLE": "Set yourself as",
|
||||
"SLA": "SLA",
|
||||
"CUSTOM_ROLES": "Custom Roles",
|
||||
"BETA": "Beta",
|
||||
"REPORTS_OVERVIEW": "Overview",
|
||||
"REAUTHORIZE": "Your inbox connection has expired, please reconnect\n to continue receiving and sending messages",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"SLA": {
|
||||
"HEADER": "SLA",
|
||||
"HEADER": "Service Level Agreements",
|
||||
"ADD_ACTION": "Add SLA",
|
||||
"ADD_ACTION_LONG": "Create a new SLA Policy",
|
||||
"DESCRIPTION": "Service Level Agreements (SLAs) are contracts that define clear expectations between your team and customers. They establish standards for response and resolution times, creating a framework for accountability and ensures a consistent, high-quality experience.",
|
||||
@@ -105,4 +105,4 @@
|
||||
"HIDE": "Hide {count} rows"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"AND": "و",
|
||||
"OR": "أو"
|
||||
},
|
||||
"INPUT_PLACEHOLDER": "Enter value",
|
||||
"INPUT_PLACEHOLDER": "أدخل القيمة",
|
||||
"OPERATOR_LABELS": {
|
||||
"equal_to": "يساوي",
|
||||
"not_equal_to": "لا يساوي",
|
||||
@@ -55,6 +55,9 @@
|
||||
"CREATED_AT": "تم إنشاؤها في",
|
||||
"LAST_ACTIVITY": "آخر نشاط"
|
||||
},
|
||||
"ERRORS": {
|
||||
"VALUE_REQUIRED": "القيمة مطلوبة"
|
||||
},
|
||||
"GROUPS": {
|
||||
"STANDARD_FILTERS": "التصفيات القياسية",
|
||||
"ADDITIONAL_FILTERS": "التصفيات الإضافية",
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
"HEADER": "الوكلاء",
|
||||
"HEADER_BTN_TXT": "إضافة وكيل",
|
||||
"LOADING": "جار جلب قائمة الوكلاء",
|
||||
"DESCRIPTION": "An agent is a member of your customer support team who can view and respond to user messages. The list below shows all the agents in your account.",
|
||||
"LEARN_MORE": "Learn about user roles",
|
||||
"DESCRIPTION": "الوكيل هو عضو في فريق دعم العملاء الذي يمكنه عرض رسائل المستخدم والرد عليها. القائمة أدناه تظهر جميع الوكلاء في حسابك.",
|
||||
"LEARN_MORE": "تعرف على أدوار المستخدم",
|
||||
"AGENT_TYPES": {
|
||||
"ADMINISTRATOR": "المدير",
|
||||
"AGENT": "وكيل الدعم"
|
||||
@@ -18,7 +18,8 @@
|
||||
"STATUS": "الحالة",
|
||||
"ACTIONS": "الإجراءات",
|
||||
"VERIFIED": "تم التحقق",
|
||||
"VERIFICATION_PENDING": "بانتظار التحقق"
|
||||
"VERIFICATION_PENDING": "بانتظار التحقق",
|
||||
"AVAILABLE_CUSTOM_ROLE": "Available custom role permissions"
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "إضافة وكيل دعم فني إلى فريقك",
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"HEADER": "صفات مخصصة",
|
||||
"HEADER_BTN_TXT": "إضافة صفة خاصة",
|
||||
"LOADING": "جلب الصفات المخصصة",
|
||||
"SIDEBAR_TXT": "<p><b>الصفات المخصصة</b> <p>صفة مخصصة تتبع الحقائق حول جهات الاتصال/المحادثة الخاصة بك - مثل خُطَّة الاشتراك. أو عندما يطلبون العنصر الأول وما إلى ذلك. <br /><br />لإنشاء صفة مخصصة، فقط انقر فوق<b>أضف صفة مخصصة.</b> يمكنك أيضا تعديل أو حذف صفة مخصصة موجودة بالنقر على زر التحرير أو الحذف.</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": "إضافة صفة خاصة",
|
||||
"SUBMIT": "إنشاء",
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
"HEADER": "سجلات التدقيق",
|
||||
"HEADER_BTN_TXT": "إضافة سجلات التدقيق",
|
||||
"LOADING": "جارٍ جلب سجلات التدقيق",
|
||||
"DESCRIPTION": "Audit Logs maintain a record of activities in your account, allowing you to track and audit your account, team, or services.",
|
||||
"LEARN_MORE": "Learn more about audit logs",
|
||||
"DESCRIPTION": "سجلات مراجعة الحسابات تحتفظ بسجل للأنشطة في حسابك، مما يسمح لك بتتبع ومراجعة حسابك أو فريقك أو خدماتك.",
|
||||
"LEARN_MORE": "معرفة المزيد عن سجلات المراجعة",
|
||||
"SEARCH_404": "لا توجد عناصر مطابقة لهذا الاستعلام",
|
||||
"SIDEBAR_TXT": "<p><b>سجلات التدقيق</b> </p><p> سجلات التدقيق هي مسارات للأحداث والإجراءات في نظام Chatwoot. </p>",
|
||||
"LIST": {
|
||||
@@ -32,7 +32,7 @@
|
||||
"EDIT": {
|
||||
"SELF": "%{agentName} غير %{attributes} الخاصة به إلى %{values}",
|
||||
"OTHER": "%{agentName} غير %{attributes} لـ %{user} إلى %{values}",
|
||||
"DELETED": "%{agentName} changed %{attributes} of a deleted user to %{values}"
|
||||
"DELETED": "%{agentName} غير %{attributes} لـ %{user} إلى %{values}"
|
||||
}
|
||||
},
|
||||
"INBOX": {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"AUTOMATION": {
|
||||
"HEADER": "الأتمتة",
|
||||
"DESCRIPTION": "Automation can replace and streamline existing processes that require manual effort, such as adding labels and assigning conversations to the most suitable agent. This allows the team to focus on their strengths while reducing time spent on routine tasks.",
|
||||
"LEARN_MORE": "Learn more about automation",
|
||||
"HEADER_BTN_TXT": "إضافة قاعدة أتمتة",
|
||||
"LOADING": "جلب قواعد الأتمتة",
|
||||
"SIDEBAR_TXT": "<p><b>قواعد الأتمتة الآليه</b> <p>يمكن للأتمتة استبدال وأتمتة العمليات القائمة التي تتطلب جهداً يدوياً. يمكنك القيام بالعديد من الأشياء مع التشغيل الآلي، بما في ذلك إضافة تسميات وتعيين المحادثة لأفضل وكيل. لذا يركز الفريق على ما يفعلونه على أفضل وجه ويقضي وقتاً قليلاً على المهام اليدوية.</p>",
|
||||
"ADD": {
|
||||
"TITLE": "إضافة قاعدة أتمتة",
|
||||
"SUBMIT": "إنشاء",
|
||||
@@ -94,8 +95,8 @@
|
||||
"DELETE_MESSAGE": "يجب أن يكون لديك على الأقل شرط واحد للحفظ",
|
||||
"TEAM_MESSAGE_INPUT_PLACEHOLDER": "اكتب رسالتك هنا",
|
||||
"TEAM_DROPDOWN_PLACEHOLDER": "اختيار فريق",
|
||||
"EMAIL_INPUT_PLACEHOLDER": "Enter email",
|
||||
"URL_INPUT_PLACEHOLDER": "Enter URL"
|
||||
"EMAIL_INPUT_PLACEHOLDER": "أدخل البريد الإلكتروني",
|
||||
"URL_INPUT_PLACEHOLDER": "أدخل رابط"
|
||||
},
|
||||
"TOGGLE": {
|
||||
"ACTIVATION_TITLE": "تفعيل قاعدة الأتمتة",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"CANNED_MGMT": {
|
||||
"HEADER": "الردود الجاهزة",
|
||||
"LEARN_MORE": "Learn more about canned responses",
|
||||
"DESCRIPTION": "Canned Responses are pre-written reply templates that help you quickly respond to a conversation. Agents can type the '/' character followed by the shortcode to insert a canned response during a conversation. ",
|
||||
"LEARN_MORE": "معرفة المزيد عن الاستجابات المعلبة",
|
||||
"DESCRIPTION": "الردود المسبقة هي قوالب رد مكتوبة مسبقاً تساعدك على الرد بسرعة على محادثة. يمكن للوكلاء كتابة حرف '/' يتبعه الرمز المختصر لإدراج رد مسبق أثناء محادثة. ",
|
||||
"HEADER_BTN_TXT": "إضافة رد جاهز",
|
||||
"LOADING": "جاري جلب الردود الجاهزة...",
|
||||
"SEARCH_404": "لا توجد عناصر مطابقة لهذا الاستعلام.",
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
"is_lesser_than": "هو أقل من",
|
||||
"days_before": "قبل x أيام"
|
||||
},
|
||||
"ERRORS": {
|
||||
"VALUE_REQUIRED": "القيمة مطلوبة"
|
||||
},
|
||||
"ATTRIBUTES": {
|
||||
"NAME": "الاسم",
|
||||
"EMAIL": "البريد الإلكتروني",
|
||||
|
||||
@@ -143,7 +143,7 @@
|
||||
"PRIVATE_MSG_INPUT": "زر Shift + Enter لإضافة سطر جديد. سيكون هذا مرئياً للوكلاء فقط",
|
||||
"MESSAGE_SIGNATURE_NOT_CONFIGURED": "لم يتم تكوين توقيع الرسالة، الرجاء تكوينه في إعدادات الملف الشخصي.",
|
||||
"CLICK_HERE": "انقر هنا للتحديث",
|
||||
"WHATSAPP_TEMPLATES": "قوالب Whatsapp"
|
||||
"WHATSAPP_TEMPLATES": "قوالب الواتساب"
|
||||
},
|
||||
"REPLYBOX": {
|
||||
"REPLY": "إضافة رد",
|
||||
@@ -241,9 +241,9 @@
|
||||
"ONBOARDING": {
|
||||
"TITLE": "اهلاً 👋، مرحباً بك في %{installationName}!",
|
||||
"DESCRIPTION": "شكرا للتسجيل. نريدك أن تحصل على أقصى استفادة من %{installationName}. إليك بعض الأشياء التي يمكنك القيام بها في %{installationName} لجعل التجربة رائعة.",
|
||||
"GREETING_MORNING": "👋 Good morning, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_AFTERNOON": "👋 Good afternoon, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_EVENING": "👋 Good evening, %{name}. Welcome to %{installationName}.",
|
||||
"GREETING_MORNING": "👋 صباح الخير، %{name}. مرحبا بك في %{installationName}.",
|
||||
"GREETING_AFTERNOON": "👋 مساء الخير، %{name}. مرحبا بك في %{installationName}.",
|
||||
"GREETING_EVENING": "👋 مساء الخير، %{name}. مرحبا بك في %{installationName}.",
|
||||
"READ_LATEST_UPDATES": "اطلع على آخر التحديثات",
|
||||
"ALL_CONVERSATION": {
|
||||
"TITLE": "جميع محادثاتك في مكان واحد",
|
||||
@@ -261,14 +261,14 @@
|
||||
"NEW_LINK": "انقر هنا لإنشاء وسوم"
|
||||
},
|
||||
"CANNED_RESPONSES": {
|
||||
"TITLE": "Create canned responses",
|
||||
"DESCRIPTION": "Pre-written quick reply templates help you quickly respond to a conversation. Agents can type the '/' character followed by the shortcode to insert a response.",
|
||||
"NEW_LINK": "Click here to create a canned response"
|
||||
"TITLE": "إدارة الردود الجاهزة",
|
||||
"DESCRIPTION": "الردود المسبقة هي قوالب رد مكتوبة مسبقاً تساعدك على الرد بسرعة على محادثة. يمكن للوكلاء كتابة حرف '/' يتبعه الرمز المختصر لإدراج رد مسبق أثناء محادثة.",
|
||||
"NEW_LINK": "انقر هنا لإنشاء استجابة مسبقة"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_SIDEBAR": {
|
||||
"ASSIGNEE_LABEL": "الوكيل المكلف",
|
||||
"SELF_ASSIGN": "إسناد لي",
|
||||
"SELF_ASSIGN": "إسناد إلي",
|
||||
"TEAM_LABEL": "الفريق المكلف",
|
||||
"SELECT": {
|
||||
"PLACEHOLDER": "لا شيء"
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"CUSTOM_ROLE": {
|
||||
"HEADER": "Custom Roles",
|
||||
"LEARN_MORE": "Learn more about custom roles",
|
||||
"DESCRIPTION": "Custom roles are roles that are created by the account owner or admin. These roles can be assigned to agents to define their access and permissions within the account. Custom roles can be created with specific permissions and access levels to suit the requirements of the organization.",
|
||||
"HEADER_BTN_TXT": "Add custom role",
|
||||
"LOADING": "Fetching custom roles...",
|
||||
"SEARCH_404": "لا توجد عناصر مطابقة لهذا الاستعلام.",
|
||||
"PAYWALL": {
|
||||
"TITLE": "Upgrade to create custom roles",
|
||||
"AVAILABLE_ON": "The custom role feature is only available in the Business and Enterprise plans.",
|
||||
"UPGRADE_PROMPT": "Upgrade your plan to get access to advanced features like team management, automations, custom attributes, and more.",
|
||||
"UPGRADE_NOW": "Upgrade now",
|
||||
"CANCEL_ANYTIME": "You can change or cancel your plan anytime"
|
||||
},
|
||||
"ENTERPRISE_PAYWALL": {
|
||||
"AVAILABLE_ON": "The custom role feature is only available in the paid plans.",
|
||||
"UPGRADE_PROMPT": "Upgrade to a paid plan to access advanced features like audit logs, agent capacity, and more.",
|
||||
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
|
||||
},
|
||||
"LIST": {
|
||||
"404": "There are no custom roles available in this account.",
|
||||
"TITLE": "Manage custom roles",
|
||||
"DESC": "Custom roles are roles that are created by the account owner or admin. These roles can be assigned to agents to define their access and permissions within the account. Custom roles can be created with specific permissions and access levels to suit the requirements of the organization.",
|
||||
"TABLE_HEADER": [
|
||||
"الاسم",
|
||||
"الوصف",
|
||||
"Permissions",
|
||||
"الإجراءات"
|
||||
]
|
||||
},
|
||||
"PERMISSIONS": {
|
||||
"CONVERSATION_MANAGE": "Manage all conversations",
|
||||
"CONVERSATION_UNASSIGNED_MANAGE": "Manage unassigned conversations and those assigned to them",
|
||||
"CONVERSATION_PARTICIPATING_MANAGE": "Manage participating conversations and those assigned to them",
|
||||
"CONTACT_MANAGE": "Manage contacts",
|
||||
"REPORT_MANAGE": "Manage reports",
|
||||
"KNOWLEDGE_BASE_MANAGE": "Manage knowledge base"
|
||||
},
|
||||
"FORM": {
|
||||
"NAME": {
|
||||
"LABEL": "الاسم",
|
||||
"PLACEHOLDER": "Please enter a name.",
|
||||
"ERROR": "الاسم مطلوب."
|
||||
},
|
||||
"DESCRIPTION": {
|
||||
"LABEL": "الوصف",
|
||||
"PLACEHOLDER": "Please enter a description.",
|
||||
"ERROR": "الوصف مطلوب."
|
||||
},
|
||||
"PERMISSIONS": {
|
||||
"LABEL": "Permissions",
|
||||
"ERROR": "Permissions are required."
|
||||
},
|
||||
"CANCEL_BUTTON_TEXT": "إلغاء",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً"
|
||||
}
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Add custom role",
|
||||
"DESC": " Custom roles allows you to create roles with specific permissions and access levels to suit the requirements of the organization.",
|
||||
"SUBMIT": "إرسال",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Custom role added successfully."
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
"BUTTON_TEXT": "تعديل",
|
||||
"TITLE": "Edit custom role",
|
||||
"DESC": " Custom roles allows you to create roles with specific permissions and access levels to suit the requirements of the organization.",
|
||||
"SUBMIT": "تحديث",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Custom role updated successfully."
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
"BUTTON_TEXT": "حذف",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Custom role deleted successfully.",
|
||||
"ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً"
|
||||
},
|
||||
"CONFIRM": {
|
||||
"TITLE": "تأكيد الحذف",
|
||||
"MESSAGE": "هل أنت متأكد من الحذف ",
|
||||
"YES": "نعم، احذف ",
|
||||
"NO": "لا، احتفظ به "
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@
|
||||
"UPLOADING": "جاري الرفع...",
|
||||
"SUCCESS": "تم رفع الصورة بنجاح",
|
||||
"ERROR": "حدث خطأ أثناء رفع الصورة",
|
||||
"UN_AUTHORIZED_ERROR": "You are not authorized to upload images",
|
||||
"ERROR_FILE_SIZE": "حجم الصورة يجب أن يكون أقل من {size}MB",
|
||||
"ERROR_FILE_FORMAT": "تنسيق الصورة يجب أن يكون jpg أو jpeg أو png",
|
||||
"ERROR_FILE_DIMENSIONS": "ينبغي أن تكون أبعاد الصورة أقل من 2000 × 2000"
|
||||
@@ -482,6 +483,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"LOADING": "Loading..."
|
||||
"LOADING": "جار التحميل..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"INBOX_MGMT": {
|
||||
"HEADER": "قنوات التواصل",
|
||||
"SIDEBAR_TXT": "<p><b>قنوات التواصل</b></p> <p> عند ربطك لموقع ويب أو صفحة فيسبوك إلى Chatwoot، يتم تسميتها <b>قناة تواصل</b>. يمكنك إنشاء قنوات تواصل غير محدودة من مختلف الأنواع في حساب Chatwoot الخاص بك. </p><p> انقر فوق <b>إضافة قناة تواصل</b> لربط موقع الويب أو صفحة فيسبوك الخاصة بك. </p><p> من لوحة الإدارة، يمكنك رؤية جميع المحادثات من جميع صناديق الوارد الخاصة بك والرد عليها من مكان موّحد عبر الضغط على علامة التبويب \"المحادثات\". </p><p> يمكنك أيضًا مشاهدة المحادثات الخاصة بصندوق وارد معين بالنقر على اسم صندوق الوارد على الجزء الجانبي من لوحة الإدارة. </p>",
|
||||
"DESCRIPTION": "القناة هي وضع الاتصال الذي يختاره العميل للتفاعل معك. صندوق الوارد هو المكان الذي تدير فيه التفاعلات لقناة معينة. ويمكن أن تشمل الاتصالات من مصادر مختلفة مثل البريد الإلكتروني، والمحادثة الحية، ووسائط الإعلام الاجتماعية.",
|
||||
"LEARN_MORE": "تعلم المزيد عن صناديق البريد",
|
||||
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
|
||||
"CLICK_TO_RECONNECT": "Click here to reconnect.",
|
||||
"LIST": {
|
||||
@@ -367,12 +368,19 @@
|
||||
"DESCRIPTION": "Click on the Sign in with Microsoft button to get started. You will redirected to the email sign in page. Once you accept the requested permissions, you would be redirected back to the inbox creation step.",
|
||||
"EMAIL_PLACEHOLDER": "Enter email address",
|
||||
"SIGN_IN": "تسجيل الدخول باستخدام Microsoft",
|
||||
"HELP": "To add your Microsoft account as a channel, you need to authenticate your Microsoft account by clicking on 'Sign in with Microsoft' ",
|
||||
"ERROR_MESSAGE": "There was an error connecting to Microsoft, please try again"
|
||||
},
|
||||
"GOOGLE": {
|
||||
"TITLE": "البريد الإلكتروني لجوجل",
|
||||
"DESCRIPTION": "انقر على تسجيل الدخول باستخدام زر جوجل للبدء. سيتم إعادة توجيهك إلى صفحة تسجيل الدخول بالبريد الإلكتروني. بمجرد قبول الأذونات المطلوبة، سيتم إعادة توجيهك إلى خطوة إنشاء علبة الوارد.",
|
||||
"SIGN_IN": "تسجيل الدخول باستخدام Google",
|
||||
"EMAIL_PLACEHOLDER": "Enter email address",
|
||||
"ERROR_MESSAGE": "حدث خطأ أثناء الاتصال بجوجل، الرجاء المحاولة مرة أخرى"
|
||||
}
|
||||
},
|
||||
"DETAILS": {
|
||||
"LOADING_FB": "جار المصادقة والربط مع الفيسبوك...",
|
||||
"ERROR_FB_LOADING": "Error loading Facebook SDK. Please disable any ad-blockers and try again from a different browser.",
|
||||
"ERROR_FB_AUTH": "حدث خطأ ما، الرجاء تحديث الصفحة...",
|
||||
"ERROR_FB_UNAUTHORIZED": "You're not authorized to perform this action. ",
|
||||
"ERROR_FB_UNAUTHORIZED_HELP": "Please ensure you have access to the Facebook page with full control. You can read more about Facebook roles <a href=\" https://www.facebook.com/help/187316341316631\">here</a>.",
|
||||
@@ -736,7 +744,20 @@
|
||||
},
|
||||
"EMAIL_PROVIDERS": {
|
||||
"MICROSOFT": "Microsoft",
|
||||
"GOOGLE": "Google",
|
||||
"OTHER_PROVIDERS": "Other Providers"
|
||||
},
|
||||
"CHANNELS": {
|
||||
"MESSENGER": "Messenger",
|
||||
"WEB_WIDGET": "الموقع الإلكتروني",
|
||||
"TWITTER_PROFILE": "تويتر",
|
||||
"TWILIO_SMS": "Twilio SMS",
|
||||
"WHATSAPP": "واتساب",
|
||||
"SMS": "SMS",
|
||||
"EMAIL": "البريد الإلكتروني",
|
||||
"TELEGRAM": "تيليجرام",
|
||||
"LINE": "Line",
|
||||
"API": "قناة API"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
},
|
||||
"WEBHOOK": {
|
||||
"SUBSCRIBED_EVENTS": "الأحداث المشتركة",
|
||||
"LEARN_MORE": "Learn more about webhooks",
|
||||
"FORM": {
|
||||
"CANCEL": "إلغاء",
|
||||
"DESC": "أحداث Webhook توفر لك معلومات في الوقت الحقيقي حول ما يحدث في حساب Chatwoot الخاص بك. الرجاء إدخال عنوان URL صالح لتكوين callback.",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"HEADER_BTN_TXT": "إضافة وسم جديد",
|
||||
"LOADING": "جار جلب الوسوم",
|
||||
"DESCRIPTION": "Labels help you categorize and prioritize conversations and leads. You can assign a label to a conversation or contact using the side panel.",
|
||||
"LEARN_MORE": "Learn more about labels",
|
||||
"LEARN_MORE": "معرفة المزيد حول التسميات",
|
||||
"SEARCH_404": "لا توجد عناصر مطابقة لهذا الاستعلام",
|
||||
"LIST": {
|
||||
"404": "لا توجد وسوم متوفرة في هذا الحساب.",
|
||||
@@ -51,7 +51,7 @@
|
||||
"ADD_SELECTED_LABELS": "إضافة الوسوم المحددة",
|
||||
"ADD_SELECTED_LABEL": "إضافة الوسوم المحددة",
|
||||
"ADD_ALL_LABELS": "إضافة جميع الأوسمة",
|
||||
"SUGGESTED_LABELS": "Suggested labels"
|
||||
"SUGGESTED_LABELS": "التسميات المقترحة"
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "إضافة وسم جديد",
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"MACROS": {
|
||||
"HEADER": "ماكروس",
|
||||
"DESCRIPTION": "الماكرو هو مجموعة من الإجراءات المحفوظة التي تساعد وكلاء خدمة العملاء على إكمال المهام بسهولة. يمكن للوكلاء تحديد مجموعة من الإجراءات مثل وضع علامة على محادثة مع تسمية، وإرسال نص بريد إلكتروني، وتحديث سمة مخصصة، إلخ. ويمكنهم تنفيذ هذه الإجراءات بنقرة واحدة.",
|
||||
"LEARN_MORE": "تعلم المزيد حول الماكرو",
|
||||
"HEADER_BTN_TXT": "إضافة ماكرو جديد",
|
||||
"HEADER_BTN_TXT_SAVE": "حفظ الماكرو",
|
||||
"LOADING": "جاري جلب الماكروس",
|
||||
"SIDEBAR_TXT": "<p><b>الماكروس</b><p>الماكرو هو مجموعة من الإجراءات المحفوظة التي تساعد وكلاء خدمة العملاء على إكمال المهام بسهولة. يمكن للوكلاء تحديد مجموعة من الإجراءات مثل وضع علامة على محادثة مع تسمية، وإرسال نص بريد إلكتروني، وتحديث سمة مخصصة، إلخ. ويمكنهم تنفيذ هذه الإجراءات بنقرة واحدة. وعندما يدير الوكلاء الكلية، يتم تنفيذ الإجراءات بالتسلسل حسب الترتيب الذي تحدده. يحسن الماكرو الإنتاجية ويزيد الاتساق في الإجراءات. </p><p>يمكن أن يكون الماكرو مفيداً بطريقتين. </p><p><b>كوكيل يساعد:</b> إذا قام وكيل بمجموعة من الإجراءات عدة مرات، يمكنهم حفظه كماكلي وتنفيذ جميع الإجراءات معاً باستخدام نقرة واحدة.</p><p><b>كخيار للدخول إلى عضوية الفريق:</b> يجب على كل وكيل إجراء العديد من الشيكات والإجراءات المختلفة خلال كل محادثة. سيكون عضو فريق الدعم الجديد من أونبواردينغ سهلا إذا كان الماكرو المحدد مسبقا متاحا على الحساب. وبدلا من وصف كل خطوة بالتفصيل، يمكن للمدير/الفريق أن يشير إلى الكتلة الكلية المستخدمة في سيناريوهات مختلفة.</p>",
|
||||
"ERROR": "حدث خطأ ما. الرجاء المحاولة مرة أخرى",
|
||||
"ORDER_INFO": "سيتم تشغيل الماكرو بالترتيب الذي تضيفه إجراءاتك. يمكنك إعادة ترتيبهم بسحبهم بواسطة المعالج بجانب كل عقدة.",
|
||||
"ADD": {
|
||||
|
||||
@@ -277,6 +277,7 @@
|
||||
"REPORTS_TEAM": "الفريق",
|
||||
"SET_AVAILABILITY_TITLE": "تعيين نفسك كـ",
|
||||
"SLA": "SLA",
|
||||
"CUSTOM_ROLES": "Custom Roles",
|
||||
"BETA": "تجريبي",
|
||||
"REPORTS_OVERVIEW": "نظرة عامة",
|
||||
"REAUTHORIZE": "انتهت صلاحية اتصال صندوق الوارد الخاص بك، يرجى إعادة الاتصال\n لمواصلة تلقي وإرسال الرسائل",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"SLA": {
|
||||
"HEADER": "SLA",
|
||||
"HEADER": "اتفاقات مستوى الخدمة",
|
||||
"ADD_ACTION": "Add SLA",
|
||||
"ADD_ACTION_LONG": "Create a new SLA Policy",
|
||||
"DESCRIPTION": "Service Level Agreements (SLAs) are contracts that define clear expectations between your team and customers. They establish standards for response and resolution times, creating a framework for accountability and ensures a consistent, high-quality experience.",
|
||||
@@ -105,4 +105,4 @@
|
||||
"HIDE": "Hide {count} rows"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "قوالب Whatsapp",
|
||||
"TITLE": "قوالب الواتساب",
|
||||
"SUBTITLE": "حدد القالب الذي تريد إرساله",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "معالجة %{templateName}"
|
||||
},
|
||||
|
||||
@@ -55,6 +55,9 @@
|
||||
"CREATED_AT": "Създаден в",
|
||||
"LAST_ACTIVITY": "Last activity"
|
||||
},
|
||||
"ERRORS": {
|
||||
"VALUE_REQUIRED": "Изисква се стойност"
|
||||
},
|
||||
"GROUPS": {
|
||||
"STANDARD_FILTERS": "Standard filters",
|
||||
"ADDITIONAL_FILTERS": "Additional filters",
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
"STATUS": "Статус",
|
||||
"ACTIONS": "Действия",
|
||||
"VERIFIED": "Проверен",
|
||||
"VERIFICATION_PENDING": "Предстои проверка"
|
||||
"VERIFICATION_PENDING": "Предстои проверка",
|
||||
"AVAILABLE_CUSTOM_ROLE": "Available custom role permissions"
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Добави агент в екипа си",
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"HEADER": "Персонализирани атрибути",
|
||||
"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": "Създаване",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"AUTOMATION": {
|
||||
"HEADER": "Automations",
|
||||
"HEADER": "Автоматизация",
|
||||
"DESCRIPTION": "Automation can replace and streamline existing processes that require manual effort, such as adding labels and assigning conversations to the most suitable agent. This allows the team to focus on their strengths while reducing time spent on routine tasks.",
|
||||
"LEARN_MORE": "Learn more about automation",
|
||||
"HEADER_BTN_TXT": "Добавяне правило за автоматизация",
|
||||
"LOADING": "Fetching automation rules",
|
||||
"SIDEBAR_TXT": "<p><b>Automation Rules</b> <p>Automation can replace and automate existing processes that require manual effort. You can do many things with automation, including adding labels and assigning conversation to the best agent. So the team focuses on what they do best and spends more little time on manual tasks.</p>",
|
||||
"ADD": {
|
||||
"TITLE": "Добавяне правило за автоматизация",
|
||||
"SUBMIT": "Създаване",
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
"is_lesser_than": "Is lesser than",
|
||||
"days_before": "Is x days before"
|
||||
},
|
||||
"ERRORS": {
|
||||
"VALUE_REQUIRED": "Изисква се стойност"
|
||||
},
|
||||
"ATTRIBUTES": {
|
||||
"NAME": "Име",
|
||||
"EMAIL": "Имейл",
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"CUSTOM_ROLE": {
|
||||
"HEADER": "Custom Roles",
|
||||
"LEARN_MORE": "Learn more about custom roles",
|
||||
"DESCRIPTION": "Custom roles are roles that are created by the account owner or admin. These roles can be assigned to agents to define their access and permissions within the account. Custom roles can be created with specific permissions and access levels to suit the requirements of the organization.",
|
||||
"HEADER_BTN_TXT": "Add custom role",
|
||||
"LOADING": "Fetching custom roles...",
|
||||
"SEARCH_404": "Няма резултати отговарящи на тази заявка.",
|
||||
"PAYWALL": {
|
||||
"TITLE": "Upgrade to create custom roles",
|
||||
"AVAILABLE_ON": "The custom role feature is only available in the Business and Enterprise plans.",
|
||||
"UPGRADE_PROMPT": "Upgrade your plan to get access to advanced features like team management, automations, custom attributes, and more.",
|
||||
"UPGRADE_NOW": "Upgrade now",
|
||||
"CANCEL_ANYTIME": "You can change or cancel your plan anytime"
|
||||
},
|
||||
"ENTERPRISE_PAYWALL": {
|
||||
"AVAILABLE_ON": "The custom role feature is only available in the paid plans.",
|
||||
"UPGRADE_PROMPT": "Upgrade to a paid plan to access advanced features like audit logs, agent capacity, and more.",
|
||||
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
|
||||
},
|
||||
"LIST": {
|
||||
"404": "There are no custom roles available in this account.",
|
||||
"TITLE": "Manage custom roles",
|
||||
"DESC": "Custom roles are roles that are created by the account owner or admin. These roles can be assigned to agents to define their access and permissions within the account. Custom roles can be created with specific permissions and access levels to suit the requirements of the organization.",
|
||||
"TABLE_HEADER": [
|
||||
"Име",
|
||||
"Описание",
|
||||
"Permissions",
|
||||
"Действия"
|
||||
]
|
||||
},
|
||||
"PERMISSIONS": {
|
||||
"CONVERSATION_MANAGE": "Manage all conversations",
|
||||
"CONVERSATION_UNASSIGNED_MANAGE": "Manage unassigned conversations and those assigned to them",
|
||||
"CONVERSATION_PARTICIPATING_MANAGE": "Manage participating conversations and those assigned to them",
|
||||
"CONTACT_MANAGE": "Manage contacts",
|
||||
"REPORT_MANAGE": "Manage reports",
|
||||
"KNOWLEDGE_BASE_MANAGE": "Manage knowledge base"
|
||||
},
|
||||
"FORM": {
|
||||
"NAME": {
|
||||
"LABEL": "Име",
|
||||
"PLACEHOLDER": "Please enter a name.",
|
||||
"ERROR": "Name is required."
|
||||
},
|
||||
"DESCRIPTION": {
|
||||
"LABEL": "Описание",
|
||||
"PLACEHOLDER": "Please enter a description.",
|
||||
"ERROR": "Description is required."
|
||||
},
|
||||
"PERMISSIONS": {
|
||||
"LABEL": "Permissions",
|
||||
"ERROR": "Permissions are required."
|
||||
},
|
||||
"CANCEL_BUTTON_TEXT": "Отмени",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "Не можа да се свърже с Woot сървър. Моля, опитайте отново по-късно"
|
||||
}
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Add custom role",
|
||||
"DESC": " Custom roles allows you to create roles with specific permissions and access levels to suit the requirements of the organization.",
|
||||
"SUBMIT": "Изпращане",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Custom role added successfully."
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
"BUTTON_TEXT": "Редактирай",
|
||||
"TITLE": "Edit custom role",
|
||||
"DESC": " Custom roles allows you to create roles with specific permissions and access levels to suit the requirements of the organization.",
|
||||
"SUBMIT": "Обновяване",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Custom role updated successfully."
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
"BUTTON_TEXT": "Изтрий",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Custom role deleted successfully.",
|
||||
"ERROR_MESSAGE": "Не можа да се свърже с Woot сървър. Моля, опитайте отново по-късно"
|
||||
},
|
||||
"CONFIRM": {
|
||||
"TITLE": "Потвърди изтриването",
|
||||
"MESSAGE": "Сигурни ли сте за изтриването ",
|
||||
"YES": "Yes, delete ",
|
||||
"NO": "No, keep "
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@
|
||||
"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": {
|
||||
@@ -367,12 +368,19 @@
|
||||
"DESCRIPTION": "Click on the Sign in with Microsoft button to get started. You will redirected to the email sign in page. Once you accept the requested permissions, you would be redirected back to the inbox creation step.",
|
||||
"EMAIL_PLACEHOLDER": "Enter email address",
|
||||
"SIGN_IN": "Sign in with Microsoft",
|
||||
"HELP": "To add your Microsoft account as a channel, you need to authenticate your Microsoft account by clicking on 'Sign in with Microsoft' ",
|
||||
"ERROR_MESSAGE": "There was an error connecting to Microsoft, please try again"
|
||||
},
|
||||
"GOOGLE": {
|
||||
"TITLE": "Google Email",
|
||||
"DESCRIPTION": "Click on the Sign in with Google button to get started. You will redirected to the email sign in page. Once you accept the requested permissions, you would be redirected back to the inbox creation step.",
|
||||
"SIGN_IN": "Sign in with Google",
|
||||
"EMAIL_PLACEHOLDER": "Enter email address",
|
||||
"ERROR_MESSAGE": "There was an error connecting to Google, please try again"
|
||||
}
|
||||
},
|
||||
"DETAILS": {
|
||||
"LOADING_FB": "Authenticating you with Facebook...",
|
||||
"ERROR_FB_LOADING": "Error loading Facebook SDK. Please disable any ad-blockers and try again from a different browser.",
|
||||
"ERROR_FB_AUTH": "Something went wrong, Please refresh page...",
|
||||
"ERROR_FB_UNAUTHORIZED": "You're not authorized to perform this action. ",
|
||||
"ERROR_FB_UNAUTHORIZED_HELP": "Please ensure you have access to the Facebook page with full control. You can read more about Facebook roles <a href=\" https://www.facebook.com/help/187316341316631\">here</a>.",
|
||||
@@ -736,7 +744,20 @@
|
||||
},
|
||||
"EMAIL_PROVIDERS": {
|
||||
"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": "Имейл",
|
||||
"TELEGRAM": "Telegram",
|
||||
"LINE": "Line",
|
||||
"API": "API Channel"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
},
|
||||
"WEBHOOK": {
|
||||
"SUBSCRIBED_EVENTS": "Subscribed Events",
|
||||
"LEARN_MORE": "Learn more about webhooks",
|
||||
"FORM": {
|
||||
"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,10 +1,11 @@
|
||||
{
|
||||
"MACROS": {
|
||||
"HEADER": "Macros",
|
||||
"DESCRIPTION": "A macro is a set of saved actions that help customer service agents easily complete tasks. The agents can define a set of actions like tagging a conversation with a label, sending an email transcript, updating a custom attribute, etc., and they can run these actions in a single click.",
|
||||
"LEARN_MORE": "Learn more about macros",
|
||||
"HEADER_BTN_TXT": "Add a new macro",
|
||||
"HEADER_BTN_TXT_SAVE": "Save macro",
|
||||
"LOADING": "Fetching macros",
|
||||
"SIDEBAR_TXT": "<p><b>Macros</b><p>A macro is a set of saved actions that help customer service agents easily complete tasks. The agents can define a set of actions like tagging a conversation with a label, sending an email transcript, updating a custom attribute, etc., and they can run these actions in a single click. When the agents run the macro, the actions would be performed sequentially in the order they are defined. Macros improve productivity and increase consistency in actions. </p><p>A macro can be helpful in 2 ways. </p><p><b>As an agent assist:</b> If an agent performs a set of actions multiple times, they can save it as a macro and execute all the actions together using a single click.</p><p><b>As an option to onboard a team member:</b> Every agent has to perform many different checks/actions during each conversation. Onboarding a new support team member will be easy if pre-defined macros are available on the account. Instead of describing each step in detail, the manager/team lead can point to the macros used in different scenarios.</p>",
|
||||
"ERROR": "Something went wrong. Please try again",
|
||||
"ORDER_INFO": "Macros will run in the order you add your actions. You can rearrange them by dragging them by the handle beside each node.",
|
||||
"ADD": {
|
||||
|
||||
@@ -277,6 +277,7 @@
|
||||
"REPORTS_TEAM": "Team",
|
||||
"SET_AVAILABILITY_TITLE": "Set yourself as",
|
||||
"SLA": "SLA",
|
||||
"CUSTOM_ROLES": "Custom Roles",
|
||||
"BETA": "Beta",
|
||||
"REPORTS_OVERVIEW": "Overview",
|
||||
"REAUTHORIZE": "Your inbox connection has expired, please reconnect\n to continue receiving and sending messages",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"SLA": {
|
||||
"HEADER": "SLA",
|
||||
"HEADER": "Service Level Agreements",
|
||||
"ADD_ACTION": "Add SLA",
|
||||
"ADD_ACTION_LONG": "Create a new SLA Policy",
|
||||
"DESCRIPTION": "Service Level Agreements (SLAs) are contracts that define clear expectations between your team and customers. They establish standards for response and resolution times, creating a framework for accountability and ensures a consistent, high-quality experience.",
|
||||
@@ -105,4 +105,4 @@
|
||||
"HIDE": "Hide {count} rows"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,9 @@
|
||||
"CREATED_AT": "Creat a",
|
||||
"LAST_ACTIVITY": "Última activitat"
|
||||
},
|
||||
"ERRORS": {
|
||||
"VALUE_REQUIRED": "El valor és necessari"
|
||||
},
|
||||
"GROUPS": {
|
||||
"STANDARD_FILTERS": "Filtres estàndard",
|
||||
"ADDITIONAL_FILTERS": "Filtres addicionals",
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
"STATUS": "Estat",
|
||||
"ACTIONS": "Accions",
|
||||
"VERIFIED": "Verificat",
|
||||
"VERIFICATION_PENDING": "Verificació pendent"
|
||||
"VERIFICATION_PENDING": "Verificació pendent",
|
||||
"AVAILABLE_CUSTOM_ROLE": "Available custom role permissions"
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Afegir agent al teu equip",
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"HEADER": "Atributs personalitzats",
|
||||
"HEADER_BTN_TXT": "Afegir atribut personalitzat",
|
||||
"LOADING": "S'estan recollint atributs personalitzats",
|
||||
"SIDEBAR_TXT": "<p><b>Atributs personalitzats</b> <p>Un atribut personalitzat fa un seguiment dels fets sobre els vostres contactes/converses, com ara el pla de subscripció, o quan van demanar el primer element, etc. <br /><br />Per crear un atribut personalitzat, només cal que feu clic a <b>Afegeix un atribut personalitzat.</b> També podeu editar o suprimir un atribut personalitzat existent fent clic al botó Edita o Suprimeix.</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": "Afegir atribut personalitzat",
|
||||
"SUBMIT": "Crear",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"AUTOMATION": {
|
||||
"HEADER": "Automatització",
|
||||
"DESCRIPTION": "Automation can replace and streamline existing processes that require manual effort, such as adding labels and assigning conversations to the most suitable agent. This allows the team to focus on their strengths while reducing time spent on routine tasks.",
|
||||
"LEARN_MORE": "Learn more about automation",
|
||||
"HEADER_BTN_TXT": "Afegeix una regla d'automatització",
|
||||
"LOADING": "S'estan recollint regles d'automatització",
|
||||
"SIDEBAR_TXT": "<p><b>Regles d'automatització</b> <p>L'automatització pot substituir i automatitzar processos existents que requereixen un esforç manual. Pots fer moltes coses amb l'automatització, com ara afegir etiquetes i assignar converses al millor agent. Així, l'equip se centra en allò que millor fa i dedica més poc temps a les tasques manuals.</p>",
|
||||
"ADD": {
|
||||
"TITLE": "Afegeix una regla d'automatització",
|
||||
"SUBMIT": "Crear",
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
"is_lesser_than": "És més petit que",
|
||||
"days_before": "És x dies abans"
|
||||
},
|
||||
"ERRORS": {
|
||||
"VALUE_REQUIRED": "El valor és necessari"
|
||||
},
|
||||
"ATTRIBUTES": {
|
||||
"NAME": "Nom",
|
||||
"EMAIL": "Correu electrònic",
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"CUSTOM_ROLE": {
|
||||
"HEADER": "Custom Roles",
|
||||
"LEARN_MORE": "Learn more about custom roles",
|
||||
"DESCRIPTION": "Custom roles are roles that are created by the account owner or admin. These roles can be assigned to agents to define their access and permissions within the account. Custom roles can be created with specific permissions and access levels to suit the requirements of the organization.",
|
||||
"HEADER_BTN_TXT": "Add custom role",
|
||||
"LOADING": "Fetching custom roles...",
|
||||
"SEARCH_404": "No hi ha cap resposta que coincideixi amb aquesta consulta.",
|
||||
"PAYWALL": {
|
||||
"TITLE": "Upgrade to create custom roles",
|
||||
"AVAILABLE_ON": "The custom role feature is only available in the Business and Enterprise plans.",
|
||||
"UPGRADE_PROMPT": "Actualitza el teu pla per accedir a funcions avançades com ara gestió d'equips, automatitzacions, atributs personalitzats i molt més.",
|
||||
"UPGRADE_NOW": "Actualitza ara",
|
||||
"CANCEL_ANYTIME": "Pots canviar o cancel·lar el teu pla en qualsevol moment"
|
||||
},
|
||||
"ENTERPRISE_PAYWALL": {
|
||||
"AVAILABLE_ON": "The custom role feature is only available in the paid plans.",
|
||||
"UPGRADE_PROMPT": "Actualitza a un pla de pagament per accedir a funcions avançades com els registres d'auditoria, la capacitat de l'agent i molt més.",
|
||||
"ASK_ADMIN": "Posa't en contacte amb el vostre administrador per obtenir l'actualització."
|
||||
},
|
||||
"LIST": {
|
||||
"404": "There are no custom roles available in this account.",
|
||||
"TITLE": "Manage custom roles",
|
||||
"DESC": "Custom roles are roles that are created by the account owner or admin. These roles can be assigned to agents to define their access and permissions within the account. Custom roles can be created with specific permissions and access levels to suit the requirements of the organization.",
|
||||
"TABLE_HEADER": [
|
||||
"Nom",
|
||||
"Descripció",
|
||||
"Permissions",
|
||||
"Accions"
|
||||
]
|
||||
},
|
||||
"PERMISSIONS": {
|
||||
"CONVERSATION_MANAGE": "Manage all conversations",
|
||||
"CONVERSATION_UNASSIGNED_MANAGE": "Manage unassigned conversations and those assigned to them",
|
||||
"CONVERSATION_PARTICIPATING_MANAGE": "Manage participating conversations and those assigned to them",
|
||||
"CONTACT_MANAGE": "Manage contacts",
|
||||
"REPORT_MANAGE": "Manage reports",
|
||||
"KNOWLEDGE_BASE_MANAGE": "Manage knowledge base"
|
||||
},
|
||||
"FORM": {
|
||||
"NAME": {
|
||||
"LABEL": "Nom",
|
||||
"PLACEHOLDER": "Please enter a name.",
|
||||
"ERROR": "El nom és obligatori."
|
||||
},
|
||||
"DESCRIPTION": {
|
||||
"LABEL": "Descripció",
|
||||
"PLACEHOLDER": "Please enter a description.",
|
||||
"ERROR": "La descripció és necessària."
|
||||
},
|
||||
"PERMISSIONS": {
|
||||
"LABEL": "Permissions",
|
||||
"ERROR": "Permissions are required."
|
||||
},
|
||||
"CANCEL_BUTTON_TEXT": "Cancel·la",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "No s'ha pogut connectar amb el servidor Woot. Torna-ho a provar més endavant"
|
||||
}
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Add custom role",
|
||||
"DESC": " Custom roles allows you to create roles with specific permissions and access levels to suit the requirements of the organization.",
|
||||
"SUBMIT": "Envia",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Custom role added successfully."
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
"BUTTON_TEXT": "Edita",
|
||||
"TITLE": "Edit custom role",
|
||||
"DESC": " Custom roles allows you to create roles with specific permissions and access levels to suit the requirements of the organization.",
|
||||
"SUBMIT": "Actualitza",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Custom role updated successfully."
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
"BUTTON_TEXT": "Esborrar",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Custom role deleted successfully.",
|
||||
"ERROR_MESSAGE": "No s'ha pogut connectar amb el servidor Woot. Torna-ho a provar més endavant"
|
||||
},
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirma la supressió",
|
||||
"MESSAGE": "N'estas segur? ",
|
||||
"YES": "Sí, esborra ",
|
||||
"NO": "No, mantén-la "
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@
|
||||
"UPLOADING": "S'està carregant...",
|
||||
"SUCCESS": "La imatge s'ha carregat correctament",
|
||||
"ERROR": "S'ha produït un error en carregar la imatge",
|
||||
"UN_AUTHORIZED_ERROR": "You are not authorized to upload images",
|
||||
"ERROR_FILE_SIZE": "La mida de la imatge ha de ser inferior a {size}MB",
|
||||
"ERROR_FILE_FORMAT": "El format de la imatge ha de ser JPG, JPEG o PNG",
|
||||
"ERROR_FILE_DIMENSIONS": "Les dimensions de la imatge han de ser inferiors a 2000 x 2000"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"INBOX_MGMT": {
|
||||
"HEADER": "Safates d'entrada",
|
||||
"SIDEBAR_TXT": "<p><b>Safata d’entrada</b></p> <p> Quan connecteu un lloc web o una pàgina de Facebook a Chatwoot, s’anomena<b>Safata d’entrada</b>. Podeu tenir bústies d’entrada il·limitades al vostre compte de Chatwoot. </p><p> Fer clic a <b>Add Safata d’entrada</b> per connectar un lloc web o una pàgina de Facebook.</p><p> Al tauler, pots veure totes les converses de totes les teves safates d'entrada en un sol lloc i respondre-hi a la pestanya `Converses`.</p><p> També pots veure converses específiques d’una safata d’entrada fent clic al nom de la safata d’entrada a l'esquerre del tauler. </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": "La teva safata d'entrada està desconnectada. No rebràs missatges nous fins que no els tornis a autoritzar.",
|
||||
"CLICK_TO_RECONNECT": "Fes clic aquí per tornar a connectar-te.",
|
||||
"LIST": {
|
||||
@@ -367,12 +368,19 @@
|
||||
"DESCRIPTION": "Fes clic al botó Inicia sessió amb Microsoft per començar. Se't redirigirà a la pàgina d'inici de sessió del correu electrònic. Un cop acceptis els permisos sol·licitats, se't redirigirà al pas de creació de la safata d'entrada.",
|
||||
"EMAIL_PLACEHOLDER": "Introdueix una adreça de correu electrònic",
|
||||
"SIGN_IN": "Inicia la sessió amb Microsoft",
|
||||
"HELP": "Per afegir el teu compte de Microsoft com a canal, has d'autenticar el teu compte de Microsoft fent clic a \"Inicia la sessió amb Microsoft\" ",
|
||||
"ERROR_MESSAGE": "S'ha produït un error en connectar amb Microsoft, torna-ho a provar"
|
||||
},
|
||||
"GOOGLE": {
|
||||
"TITLE": "Google Email",
|
||||
"DESCRIPTION": "Click on the Sign in with Google button to get started. You will redirected to the email sign in page. Once you accept the requested permissions, you would be redirected back to the inbox creation step.",
|
||||
"SIGN_IN": "Sign in with Google",
|
||||
"EMAIL_PLACEHOLDER": "Introdueix una adreça de correu electrònic",
|
||||
"ERROR_MESSAGE": "There was an error connecting to Google, please try again"
|
||||
}
|
||||
},
|
||||
"DETAILS": {
|
||||
"LOADING_FB": "S'està autenticant amb Facebook...",
|
||||
"ERROR_FB_LOADING": "Error loading Facebook SDK. Please disable any ad-blockers and try again from a different browser.",
|
||||
"ERROR_FB_AUTH": "Alguna cosa ha anat malament, actualitza la pàgina...",
|
||||
"ERROR_FB_UNAUTHORIZED": "No estàs autoritzat per dur a terme aquesta acció. ",
|
||||
"ERROR_FB_UNAUTHORIZED_HELP": "Assegura't que tens accés a la pàgina de Facebook amb control total. Pots obtenir més informació sobre les funcions de Facebook <a href=\" https://www.facebook.com/help/187316341316631\">aquí</a>.",
|
||||
@@ -736,7 +744,20 @@
|
||||
},
|
||||
"EMAIL_PROVIDERS": {
|
||||
"MICROSOFT": "Microsoft",
|
||||
"GOOGLE": "Google",
|
||||
"OTHER_PROVIDERS": "Altres proveïdors"
|
||||
},
|
||||
"CHANNELS": {
|
||||
"MESSENGER": "Messenger",
|
||||
"WEB_WIDGET": "Lloc web",
|
||||
"TWITTER_PROFILE": "Twitter",
|
||||
"TWILIO_SMS": "Twilio SMS",
|
||||
"WHATSAPP": "WhatsApp",
|
||||
"SMS": "SMS",
|
||||
"EMAIL": "Correu electrònic",
|
||||
"TELEGRAM": "Telegram",
|
||||
"LINE": "Line",
|
||||
"API": "Canal de l'API"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
},
|
||||
"WEBHOOK": {
|
||||
"SUBSCRIBED_EVENTS": "Esdeveniments subscrits",
|
||||
"LEARN_MORE": "Learn more about webhooks",
|
||||
"FORM": {
|
||||
"CANCEL": "Cancel·la",
|
||||
"DESC": "Els esdeveniments de Webhook us proporcionen informació en temps real sobre el que passa al vostre compte de Chatwoot. Introduïu una URL vàlid per configurar un callback.",
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"MACROS": {
|
||||
"HEADER": "Macros",
|
||||
"DESCRIPTION": "A macro is a set of saved actions that help customer service agents easily complete tasks. The agents can define a set of actions like tagging a conversation with a label, sending an email transcript, updating a custom attribute, etc., and they can run these actions in a single click.",
|
||||
"LEARN_MORE": "Learn more about macros",
|
||||
"HEADER_BTN_TXT": "Afegeix una nova macro",
|
||||
"HEADER_BTN_TXT_SAVE": "Desa la macro",
|
||||
"LOADING": "Obtenció de macros",
|
||||
"SIDEBAR_TXT": "<p><b>Macros</b><p>Una macro és un conjunt d'accions desades que ajuden els agents d'atenció al client a completar tasques fàcilment. Els agents poden definir un conjunt d'accions com etiquetar una conversa amb una etiqueta, enviar una transcripció de correu electrònic, actualitzar un atribut personalitzat, etc., i poden executar aquestes accions amb un sol clic. Quan els agents executen la macro, les accions es realitzaran seqüencialment en l'ordre en què es defineixen. Les macros milloren la productivitat i augmenten la coherència en les accions. </p><p>Una macro pot ser útil de dues maneres. </p><p><b>Com a ajuda d'un agent:</b> si un agent realitza un conjunt d'accions diverses vegades, pot desar-lo com a macro i executar totes les accions junts amb un sol clic.</ p><p><b>Com a opció per incorporar un membre de l'equip:</b> Cada agent ha de realitzar moltes comprovacions/accions diferents durant cada conversa. La incorporació d'un nou membre de l'equip d'assistència serà fàcil si hi ha macros predefinides disponibles al compte. En lloc de descriure cada pas en detall, el responsable o el responsable de l'equip pot indicar les macros utilitzades en diferents escenaris.</p>",
|
||||
"ERROR": "Alguna cosa ha anat malament, torna-ho a provar",
|
||||
"ORDER_INFO": "Les macros s'executaran en l'ordre en què afegeixis les teves accions. Pots reorganitzar-los arrossegant-los per l'identificador al costat de cada node.",
|
||||
"ADD": {
|
||||
|
||||
@@ -277,6 +277,7 @@
|
||||
"REPORTS_TEAM": "Equip",
|
||||
"SET_AVAILABILITY_TITLE": "Posa't com",
|
||||
"SLA": "SLA",
|
||||
"CUSTOM_ROLES": "Custom Roles",
|
||||
"BETA": "Beta",
|
||||
"REPORTS_OVERVIEW": "Resum",
|
||||
"REAUTHORIZE": "La vostra connexió a la safata d'entrada ha caducat, torna a connectar\n per continuar rebent i enviant missatges",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"SLA": {
|
||||
"HEADER": "SLA",
|
||||
"HEADER": "Service Level Agreements",
|
||||
"ADD_ACTION": "Afegeix SLA",
|
||||
"ADD_ACTION_LONG": "Crea una nova política de SLA",
|
||||
"DESCRIPTION": "Els acords de nivell de servei (SLA) són contractes que defineixen expectatives clares entre el vostre equip i els clients. Estableixen estàndards de temps de resposta i resolució, creen un marc de responsabilitat i garanteixen una experiència coherent i d'alta qualitat.",
|
||||
@@ -105,4 +105,4 @@
|
||||
"HIDE": "Amaga {count} files"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,9 @@
|
||||
"CREATED_AT": "Created at",
|
||||
"LAST_ACTIVITY": "Last activity"
|
||||
},
|
||||
"ERRORS": {
|
||||
"VALUE_REQUIRED": "Hodnota je povinná"
|
||||
},
|
||||
"GROUPS": {
|
||||
"STANDARD_FILTERS": "Standard filters",
|
||||
"ADDITIONAL_FILTERS": "Additional filters",
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
"STATUS": "Stav",
|
||||
"ACTIONS": "Akce",
|
||||
"VERIFIED": "Ověřeno",
|
||||
"VERIFICATION_PENDING": "Probíhá ověření"
|
||||
"VERIFICATION_PENDING": "Probíhá ověření",
|
||||
"AVAILABLE_CUSTOM_ROLE": "Available custom role permissions"
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Přidat agenta do vašeho týmu",
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"HEADER": "Vlastní atributy",
|
||||
"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",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"AUTOMATION": {
|
||||
"HEADER": "Automations",
|
||||
"HEADER": "Automation",
|
||||
"DESCRIPTION": "Automation can replace and streamline existing processes that require manual effort, such as adding labels and assigning conversations to the most suitable agent. This allows the team to focus on their strengths while reducing time spent on routine tasks.",
|
||||
"LEARN_MORE": "Learn more about automation",
|
||||
"HEADER_BTN_TXT": "Add Automation Rule",
|
||||
"LOADING": "Fetching automation rules",
|
||||
"SIDEBAR_TXT": "<p><b>Automation Rules</b> <p>Automation can replace and automate existing processes that require manual effort. You can do many things with automation, including adding labels and assigning conversation to the best agent. So the team focuses on what they do best and spends more little time on manual tasks.</p>",
|
||||
"ADD": {
|
||||
"TITLE": "Add Automation Rule",
|
||||
"SUBMIT": "Create",
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
"is_lesser_than": "Je menší než",
|
||||
"days_before": "Je o x dnů dříve"
|
||||
},
|
||||
"ERRORS": {
|
||||
"VALUE_REQUIRED": "Hodnota je povinná"
|
||||
},
|
||||
"ATTRIBUTES": {
|
||||
"NAME": "Název",
|
||||
"EMAIL": "E-mailová adresa",
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"CUSTOM_ROLE": {
|
||||
"HEADER": "Custom Roles",
|
||||
"LEARN_MORE": "Learn more about custom roles",
|
||||
"DESCRIPTION": "Custom roles are roles that are created by the account owner or admin. These roles can be assigned to agents to define their access and permissions within the account. Custom roles can be created with specific permissions and access levels to suit the requirements of the organization.",
|
||||
"HEADER_BTN_TXT": "Add custom role",
|
||||
"LOADING": "Fetching custom roles...",
|
||||
"SEARCH_404": "Neexistují žádné položky odpovídající tomuto dotazu.",
|
||||
"PAYWALL": {
|
||||
"TITLE": "Upgrade to create custom roles",
|
||||
"AVAILABLE_ON": "The custom role feature is only available in the Business and Enterprise plans.",
|
||||
"UPGRADE_PROMPT": "Upgrade your plan to get access to advanced features like team management, automations, custom attributes, and more.",
|
||||
"UPGRADE_NOW": "Upgrade now",
|
||||
"CANCEL_ANYTIME": "You can change or cancel your plan anytime"
|
||||
},
|
||||
"ENTERPRISE_PAYWALL": {
|
||||
"AVAILABLE_ON": "The custom role feature is only available in the paid plans.",
|
||||
"UPGRADE_PROMPT": "Upgrade to a paid plan to access advanced features like audit logs, agent capacity, and more.",
|
||||
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
|
||||
},
|
||||
"LIST": {
|
||||
"404": "There are no custom roles available in this account.",
|
||||
"TITLE": "Manage custom roles",
|
||||
"DESC": "Custom roles are roles that are created by the account owner or admin. These roles can be assigned to agents to define their access and permissions within the account. Custom roles can be created with specific permissions and access levels to suit the requirements of the organization.",
|
||||
"TABLE_HEADER": [
|
||||
"Název",
|
||||
"Description",
|
||||
"Permissions",
|
||||
"Akce"
|
||||
]
|
||||
},
|
||||
"PERMISSIONS": {
|
||||
"CONVERSATION_MANAGE": "Manage all conversations",
|
||||
"CONVERSATION_UNASSIGNED_MANAGE": "Manage unassigned conversations and those assigned to them",
|
||||
"CONVERSATION_PARTICIPATING_MANAGE": "Manage participating conversations and those assigned to them",
|
||||
"CONTACT_MANAGE": "Manage contacts",
|
||||
"REPORT_MANAGE": "Manage reports",
|
||||
"KNOWLEDGE_BASE_MANAGE": "Manage knowledge base"
|
||||
},
|
||||
"FORM": {
|
||||
"NAME": {
|
||||
"LABEL": "Název",
|
||||
"PLACEHOLDER": "Please enter a name.",
|
||||
"ERROR": "Name is required."
|
||||
},
|
||||
"DESCRIPTION": {
|
||||
"LABEL": "Description",
|
||||
"PLACEHOLDER": "Please enter a description.",
|
||||
"ERROR": "Description is required."
|
||||
},
|
||||
"PERMISSIONS": {
|
||||
"LABEL": "Permissions",
|
||||
"ERROR": "Permissions are required."
|
||||
},
|
||||
"CANCEL_BUTTON_TEXT": "Zrušit",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "Nelze se připojit k Woot serveru, opakujte akci později"
|
||||
}
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Add custom role",
|
||||
"DESC": " Custom roles allows you to create roles with specific permissions and access levels to suit the requirements of the organization.",
|
||||
"SUBMIT": "Odeslat",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Custom role added successfully."
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
"BUTTON_TEXT": "Upravit",
|
||||
"TITLE": "Edit custom role",
|
||||
"DESC": " Custom roles allows you to create roles with specific permissions and access levels to suit the requirements of the organization.",
|
||||
"SUBMIT": "Aktualizovat",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Custom role updated successfully."
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
"BUTTON_TEXT": "Vymazat",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Custom role deleted successfully.",
|
||||
"ERROR_MESSAGE": "Nelze se připojit k Woot serveru, opakujte akci později"
|
||||
},
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirm deletion",
|
||||
"MESSAGE": "Opravdu chcete odstranit ",
|
||||
"YES": "Yes, delete ",
|
||||
"NO": "No, keep "
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@
|
||||
"UPLOADING": "Nahrávání...",
|
||||
"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": "Schránky",
|
||||
"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": {
|
||||
@@ -367,12 +368,19 @@
|
||||
"DESCRIPTION": "Click on the Sign in with Microsoft button to get started. You will redirected to the email sign in page. Once you accept the requested permissions, you would be redirected back to the inbox creation step.",
|
||||
"EMAIL_PLACEHOLDER": "Enter email address",
|
||||
"SIGN_IN": "Sign in with Microsoft",
|
||||
"HELP": "To add your Microsoft account as a channel, you need to authenticate your Microsoft account by clicking on 'Sign in with Microsoft' ",
|
||||
"ERROR_MESSAGE": "There was an error connecting to Microsoft, please try again"
|
||||
},
|
||||
"GOOGLE": {
|
||||
"TITLE": "Google Email",
|
||||
"DESCRIPTION": "Click on the Sign in with Google button to get started. You will redirected to the email sign in page. Once you accept the requested permissions, you would be redirected back to the inbox creation step.",
|
||||
"SIGN_IN": "Sign in with Google",
|
||||
"EMAIL_PLACEHOLDER": "Enter email address",
|
||||
"ERROR_MESSAGE": "There was an error connecting to Google, please try again"
|
||||
}
|
||||
},
|
||||
"DETAILS": {
|
||||
"LOADING_FB": "Ověřování pomocí Facebooku...",
|
||||
"ERROR_FB_LOADING": "Error loading Facebook SDK. Please disable any ad-blockers and try again from a different browser.",
|
||||
"ERROR_FB_AUTH": "Něco se pokazilo, prosím obnovte stránku...",
|
||||
"ERROR_FB_UNAUTHORIZED": "You're not authorized to perform this action. ",
|
||||
"ERROR_FB_UNAUTHORIZED_HELP": "Please ensure you have access to the Facebook page with full control. You can read more about Facebook roles <a href=\" https://www.facebook.com/help/187316341316631\">here</a>.",
|
||||
@@ -736,7 +744,20 @@
|
||||
},
|
||||
"EMAIL_PROVIDERS": {
|
||||
"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": "E-mailová adresa",
|
||||
"TELEGRAM": "Telegram",
|
||||
"LINE": "Line",
|
||||
"API": "API Channel"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user