Compare commits
56
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6aea9e0da | ||
|
|
a801eb8c44 | ||
|
|
4019ccd678 | ||
|
|
b2eca91c79 | ||
|
|
aee0740bcc | ||
|
|
96b5780ea7 | ||
|
|
d451615811 | ||
|
|
7d68c25c97 | ||
|
|
3e560cb4cc | ||
|
|
a8b302d4cd | ||
|
|
da42a4e4a1 | ||
|
|
8eb0558b0a | ||
|
|
ee7187d2ed | ||
|
|
4e0b091ef8 | ||
|
|
daaa18b18a | ||
|
|
bddf06907b | ||
|
|
1a220b2982 | ||
|
|
c483034a07 | ||
|
|
7b51939f07 | ||
|
|
821a5b85c2 | ||
|
|
0917e1a646 | ||
|
|
9407cc2ad5 | ||
|
|
ff68c3a74f | ||
|
|
58cec84b93 | ||
|
|
34b42a1ce1 | ||
|
|
ab83a663f0 | ||
|
|
b099d3a1eb | ||
|
|
d526cf283d | ||
|
|
a2e348df06 | ||
|
|
f5957e7970 | ||
|
|
8b230c6920 | ||
|
|
c7da5b4cde | ||
|
|
b0863ab1cd | ||
|
|
005a22fd69 | ||
|
|
ba3eb787e7 | ||
|
|
837026c146 | ||
|
|
8311657f9c | ||
|
|
73140998ff | ||
|
|
4e9f644646 | ||
|
|
bfa8a8ed60 | ||
|
|
7ab60d9f9c | ||
|
|
faac1486e9 | ||
|
|
8340bd615c | ||
|
|
75d3569f22 | ||
|
|
545453537f | ||
|
|
d75702c6b2 | ||
|
|
b76ec878f1 | ||
|
|
a954e1eaca | ||
|
|
bc5f1722e1 | ||
|
|
8f39e62570 | ||
|
|
7520ca7a99 | ||
|
|
35f4e63605 | ||
|
|
5773089865 | ||
|
|
d01c7d3fa7 | ||
|
|
959d2c0d8c | ||
|
|
622f29a0b7 |
@@ -76,7 +76,7 @@ jobs:
|
||||
bundle install
|
||||
|
||||
- node/install:
|
||||
node-version: '23.7'
|
||||
node-version: '24.13'
|
||||
- node/install-pnpm
|
||||
- node/install-packages:
|
||||
pkg-manager: pnpm
|
||||
@@ -117,7 +117,7 @@ jobs:
|
||||
steps:
|
||||
- checkout
|
||||
- node/install:
|
||||
node-version: '23.7'
|
||||
node-version: '24.13'
|
||||
- node/install-pnpm
|
||||
- node/install-packages:
|
||||
pkg-manager: pnpm
|
||||
@@ -148,7 +148,7 @@ jobs:
|
||||
steps:
|
||||
- checkout
|
||||
- node/install:
|
||||
node-version: '23.7'
|
||||
node-version: '24.13'
|
||||
- node/install-pnpm
|
||||
- node/install-packages:
|
||||
pkg-manager: pnpm
|
||||
|
||||
@@ -10,7 +10,7 @@ services:
|
||||
dockerfile: .devcontainer/Dockerfile.base
|
||||
args:
|
||||
VARIANT: 'ubuntu-22.04'
|
||||
NODE_VERSION: '23.7.0'
|
||||
NODE_VERSION: '24.13.0'
|
||||
RUBY_VERSION: '3.4.4'
|
||||
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
|
||||
USER_UID: '1000'
|
||||
|
||||
@@ -11,7 +11,7 @@ services:
|
||||
dockerfile: .devcontainer/Dockerfile
|
||||
args:
|
||||
VARIANT: 'ubuntu-22.04'
|
||||
NODE_VERSION: '23.7.0'
|
||||
NODE_VERSION: '24.13.0'
|
||||
RUBY_VERSION: '3.4.4'
|
||||
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
|
||||
USER_UID: '1000'
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 23
|
||||
node-version: 24
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install pnpm dependencies
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 23
|
||||
node-version: 24
|
||||
cache: 'pnpm'
|
||||
- name: Install pnpm dependencies
|
||||
run: pnpm i
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 23
|
||||
node-version: 24
|
||||
cache: 'pnpm'
|
||||
- name: Install pnpm dependencies
|
||||
run: pnpm i
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 23
|
||||
node-version: 24
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install pnpm dependencies
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 23
|
||||
node-version: 24
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: pnpm
|
||||
|
||||
@@ -191,10 +191,10 @@ gem 'reverse_markdown'
|
||||
|
||||
gem 'iso-639'
|
||||
gem 'ruby-openai'
|
||||
gem 'ai-agents', '>= 0.8.0'
|
||||
gem 'ai-agents', '>= 0.7.0'
|
||||
|
||||
# TODO: Move this gem as a dependency of ai-agents
|
||||
gem 'ruby_llm', '>= 1.9.1'
|
||||
gem 'ruby_llm', '>= 1.8.2'
|
||||
gem 'ruby_llm-schema'
|
||||
|
||||
# OpenTelemetry for LLM observability
|
||||
|
||||
+22
-20
@@ -126,8 +126,8 @@ GEM
|
||||
jbuilder (~> 2)
|
||||
rails (>= 4.2, < 7.2)
|
||||
selectize-rails (~> 0.6)
|
||||
ai-agents (0.8.0)
|
||||
ruby_llm (~> 1.9.1)
|
||||
ai-agents (0.7.0)
|
||||
ruby_llm (~> 1.8.2)
|
||||
annotaterb (4.20.0)
|
||||
activerecord (>= 6.0.0)
|
||||
activesupport (>= 6.0.0)
|
||||
@@ -297,7 +297,7 @@ GEM
|
||||
railties (>= 5.0.0)
|
||||
faker (3.2.0)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (2.14.0)
|
||||
faraday (2.13.1)
|
||||
faraday-net_http (>= 2.0, < 3.5)
|
||||
json
|
||||
logger
|
||||
@@ -306,14 +306,14 @@ GEM
|
||||
faraday-mashify (1.0.0)
|
||||
faraday (~> 2.0)
|
||||
hashie
|
||||
faraday-multipart (1.2.0)
|
||||
multipart-post (~> 2.0)
|
||||
faraday-net_http (3.4.2)
|
||||
net-http (~> 0.5)
|
||||
faraday-multipart (1.0.4)
|
||||
multipart-post (~> 2)
|
||||
faraday-net_http (3.4.0)
|
||||
net-http (>= 0.5.0)
|
||||
faraday-net_http_persistent (2.1.0)
|
||||
faraday (~> 2.5)
|
||||
net-http-persistent (~> 4.0)
|
||||
faraday-retry (2.4.0)
|
||||
faraday-retry (2.2.1)
|
||||
faraday (~> 2.0)
|
||||
faraday_middleware-aws-sigv4 (1.0.1)
|
||||
aws-sigv4 (~> 1.0)
|
||||
@@ -441,7 +441,8 @@ GEM
|
||||
http-cookie (1.0.5)
|
||||
domain_name (~> 0.5)
|
||||
http-form_data (2.3.0)
|
||||
httparty (0.21.0)
|
||||
httparty (0.24.0)
|
||||
csv
|
||||
mini_mime (>= 1.0.0)
|
||||
multi_xml (>= 0.5.2)
|
||||
httpclient (2.8.3)
|
||||
@@ -463,7 +464,7 @@ GEM
|
||||
rails-dom-testing (>= 1, < 3)
|
||||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
json (2.18.0)
|
||||
json (2.13.2)
|
||||
json_refs (0.1.8)
|
||||
hana
|
||||
json_schemer (0.2.24)
|
||||
@@ -538,7 +539,7 @@ GEM
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
marcel (1.1.0)
|
||||
marcel (1.0.4)
|
||||
maxminddb (0.1.22)
|
||||
meta_request (0.8.3)
|
||||
rack-contrib (>= 1.1, < 3)
|
||||
@@ -555,13 +556,14 @@ GEM
|
||||
ruby2_keywords
|
||||
msgpack (1.8.0)
|
||||
multi_json (1.15.0)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.4.1)
|
||||
multi_xml (0.8.0)
|
||||
bigdecimal (>= 3.1, < 5)
|
||||
multipart-post (2.3.0)
|
||||
mutex_m (0.3.0)
|
||||
neighbor (0.2.3)
|
||||
activerecord (>= 5.2)
|
||||
net-http (0.9.1)
|
||||
uri (>= 0.11.1)
|
||||
net-http (0.6.0)
|
||||
uri
|
||||
net-http-persistent (4.0.2)
|
||||
connection_pool (~> 2.2)
|
||||
net-imap (0.4.20)
|
||||
@@ -822,7 +824,7 @@ GEM
|
||||
ruby2ruby (2.5.0)
|
||||
ruby_parser (~> 3.1)
|
||||
sexp_processor (~> 4.6)
|
||||
ruby_llm (1.9.1)
|
||||
ruby_llm (1.8.2)
|
||||
base64
|
||||
event_stream_parser (~> 1)
|
||||
faraday (>= 1.10.0)
|
||||
@@ -966,7 +968,7 @@ GEM
|
||||
unicode-emoji (~> 4.0, >= 4.0.4)
|
||||
unicode-emoji (4.0.4)
|
||||
uniform_notifier (1.17.0)
|
||||
uri (1.1.1)
|
||||
uri (1.0.4)
|
||||
uri_template (0.7.0)
|
||||
valid_email2 (5.2.6)
|
||||
activemodel (>= 3.2)
|
||||
@@ -1001,7 +1003,7 @@ GEM
|
||||
working_hours (1.4.1)
|
||||
activesupport (>= 3.2)
|
||||
tzinfo
|
||||
zeitwerk (2.7.4)
|
||||
zeitwerk (2.6.17)
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin-20
|
||||
@@ -1021,7 +1023,7 @@ DEPENDENCIES
|
||||
administrate (>= 0.20.1)
|
||||
administrate-field-active_storage (>= 1.0.3)
|
||||
administrate-field-belongs_to_search (>= 0.9.0)
|
||||
ai-agents (>= 0.8.0)
|
||||
ai-agents (>= 0.7.0)
|
||||
annotaterb
|
||||
attr_extras
|
||||
audited (~> 5.4, >= 5.4.1)
|
||||
@@ -1123,7 +1125,7 @@ DEPENDENCIES
|
||||
rubocop-rails
|
||||
rubocop-rspec
|
||||
ruby-openai
|
||||
ruby_llm (>= 1.9.1)
|
||||
ruby_llm (>= 1.8.2)
|
||||
ruby_llm-schema
|
||||
scout_apm
|
||||
scss_lint
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
4.9.1
|
||||
4.10.0
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
3.4.3
|
||||
3.5.0
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
class V2::Reports::ChannelSummaryBuilder
|
||||
include DateRangeHelper
|
||||
|
||||
pattr_initialize [:account!, :params!]
|
||||
|
||||
def build
|
||||
conversations_by_channel_and_status.transform_values { |status_counts| build_channel_stats(status_counts) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def conversations_by_channel_and_status
|
||||
account.conversations
|
||||
.joins(:inbox)
|
||||
.where(created_at: range)
|
||||
.group('inboxes.channel_type', 'conversations.status')
|
||||
.count
|
||||
.each_with_object({}) do |((channel_type, status), count), grouped|
|
||||
grouped[channel_type] ||= {}
|
||||
grouped[channel_type][status] = count
|
||||
end
|
||||
end
|
||||
|
||||
def build_channel_stats(status_counts)
|
||||
open_count = status_counts['open'] || 0
|
||||
resolved_count = status_counts['resolved'] || 0
|
||||
pending_count = status_counts['pending'] || 0
|
||||
snoozed_count = status_counts['snoozed'] || 0
|
||||
|
||||
{
|
||||
open: open_count,
|
||||
resolved: resolved_count,
|
||||
pending: pending_count,
|
||||
snoozed: snoozed_count,
|
||||
total: open_count + resolved_count + pending_count + snoozed_count
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,76 @@
|
||||
class Api::V1::Accounts::Captain::PreferencesController < Api::V1::Accounts::BaseController
|
||||
before_action :current_account
|
||||
before_action :authorize_account_update, only: [:update]
|
||||
|
||||
def show
|
||||
render json: preferences_payload
|
||||
end
|
||||
|
||||
def update
|
||||
params_to_update = captain_params
|
||||
@current_account.captain_models = params_to_update[:captain_models] if params_to_update[:captain_models]
|
||||
@current_account.captain_features = params_to_update[:captain_features] if params_to_update[:captain_features]
|
||||
@current_account.save!
|
||||
|
||||
render json: preferences_payload
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def preferences_payload
|
||||
{
|
||||
providers: Llm::Models.providers,
|
||||
models: Llm::Models.models,
|
||||
features: features_with_account_preferences
|
||||
}
|
||||
end
|
||||
|
||||
def authorize_account_update
|
||||
authorize @current_account, :update?
|
||||
end
|
||||
|
||||
def captain_params
|
||||
permitted = {}
|
||||
permitted[:captain_models] = merged_captain_models if params[:captain_models].present?
|
||||
permitted[:captain_features] = merged_captain_features if params[:captain_features].present?
|
||||
permitted
|
||||
end
|
||||
|
||||
def merged_captain_models
|
||||
existing_models = @current_account.captain_models || {}
|
||||
existing_models.merge(permitted_captain_models)
|
||||
end
|
||||
|
||||
def merged_captain_features
|
||||
existing_features = @current_account.captain_features || {}
|
||||
existing_features.merge(permitted_captain_features)
|
||||
end
|
||||
|
||||
def permitted_captain_models
|
||||
params.require(:captain_models).permit(
|
||||
:editor, :assistant, :copilot, :label_suggestion,
|
||||
:audio_transcription, :help_center_search
|
||||
).to_h.stringify_keys
|
||||
end
|
||||
|
||||
def permitted_captain_features
|
||||
params.require(:captain_features).permit(
|
||||
:editor, :assistant, :copilot, :label_suggestion,
|
||||
:audio_transcription, :help_center_search
|
||||
).to_h.stringify_keys
|
||||
end
|
||||
|
||||
def features_with_account_preferences
|
||||
preferences = Current.account.captain_preferences
|
||||
account_features = preferences[:features] || {}
|
||||
account_models = preferences[:models] || {}
|
||||
|
||||
Llm::Models.feature_keys.index_with do |feature_key|
|
||||
config = Llm::Models.feature_config(feature_key)
|
||||
config.merge(
|
||||
enabled: account_features[feature_key] == true,
|
||||
selected: account_models[feature_key] || config[:default]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -50,3 +50,5 @@ class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::Base
|
||||
@current_page = params[:page] || 1
|
||||
end
|
||||
end
|
||||
|
||||
Api::V1::Accounts::CsatSurveyResponsesController.prepend_mod_with('Api::V1::Accounts::CsatSurveyResponsesController')
|
||||
|
||||
@@ -1,38 +1,27 @@
|
||||
class Api::V1::Accounts::InboxCsatTemplatesController < Api::V1::Accounts::BaseController
|
||||
DEFAULT_BUTTON_TEXT = 'Please rate us'.freeze
|
||||
DEFAULT_LANGUAGE = 'en'.freeze
|
||||
|
||||
before_action :fetch_inbox
|
||||
before_action :validate_whatsapp_channel
|
||||
|
||||
def show
|
||||
template = @inbox.csat_config&.dig('template')
|
||||
return render json: { template_exists: false } unless template
|
||||
service = CsatTemplateManagementService.new(@inbox)
|
||||
result = service.template_status
|
||||
|
||||
template_name = template['name'] || Whatsapp::CsatTemplateNameService.csat_template_name(@inbox.id)
|
||||
status_result = @inbox.channel.provider_service.get_template_status(template_name)
|
||||
|
||||
render_template_status_response(status_result, template_name)
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Error fetching CSAT template status: #{e.message}"
|
||||
render json: { error: e.message }, status: :internal_server_error
|
||||
if result[:service_error]
|
||||
render json: { error: result[:service_error] }, status: :internal_server_error
|
||||
else
|
||||
render json: result
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
template_params = extract_template_params
|
||||
return render_missing_message_error if template_params[:message].blank?
|
||||
|
||||
# Delete existing template even though we are using a new one.
|
||||
# We don't want too many templates in the business portfolio, but the create operation shouldn't fail if deletion fails.
|
||||
delete_existing_template_if_needed
|
||||
|
||||
result = create_template_via_provider(template_params)
|
||||
service = CsatTemplateManagementService.new(@inbox)
|
||||
result = service.create_template(template_params)
|
||||
render_template_creation_result(result)
|
||||
rescue ActionController::ParameterMissing
|
||||
render json: { error: 'Template parameters are required' }, status: :unprocessable_entity
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Error creating CSAT template: #{e.message}"
|
||||
render json: { error: 'Template creation failed' }, status: :internal_server_error
|
||||
end
|
||||
|
||||
private
|
||||
@@ -43,9 +32,9 @@ class Api::V1::Accounts::InboxCsatTemplatesController < Api::V1::Accounts::BaseC
|
||||
end
|
||||
|
||||
def validate_whatsapp_channel
|
||||
return if @inbox.whatsapp?
|
||||
return if @inbox.whatsapp? || @inbox.twilio_whatsapp?
|
||||
|
||||
render json: { error: 'CSAT template operations only available for WhatsApp channels' },
|
||||
render json: { error: 'CSAT template operations only available for WhatsApp and Twilio WhatsApp channels' },
|
||||
status: :bad_request
|
||||
end
|
||||
|
||||
@@ -57,35 +46,36 @@ class Api::V1::Accounts::InboxCsatTemplatesController < Api::V1::Accounts::BaseC
|
||||
render json: { error: 'Message is required' }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def create_template_via_provider(template_params)
|
||||
template_config = {
|
||||
message: template_params[:message],
|
||||
button_text: template_params[:button_text] || DEFAULT_BUTTON_TEXT,
|
||||
base_url: ENV.fetch('FRONTEND_URL', 'http://localhost:3000'),
|
||||
language: template_params[:language] || DEFAULT_LANGUAGE,
|
||||
template_name: Whatsapp::CsatTemplateNameService.csat_template_name(@inbox.id)
|
||||
}
|
||||
|
||||
@inbox.channel.provider_service.create_csat_template(template_config)
|
||||
end
|
||||
|
||||
def render_template_creation_result(result)
|
||||
if result[:success]
|
||||
render_successful_template_creation(result)
|
||||
elsif result[:service_error]
|
||||
render json: { error: result[:service_error] }, status: :internal_server_error
|
||||
else
|
||||
render_failed_template_creation(result)
|
||||
end
|
||||
end
|
||||
|
||||
def render_successful_template_creation(result)
|
||||
render json: {
|
||||
template: {
|
||||
name: result[:template_name],
|
||||
template_id: result[:template_id],
|
||||
status: 'PENDING',
|
||||
language: result[:language] || DEFAULT_LANGUAGE
|
||||
}
|
||||
}, status: :created
|
||||
if @inbox.twilio_whatsapp?
|
||||
render json: {
|
||||
template: {
|
||||
friendly_name: result[:friendly_name],
|
||||
content_sid: result[:content_sid],
|
||||
status: result[:status] || 'pending',
|
||||
language: result[:language] || 'en'
|
||||
}
|
||||
}, status: :created
|
||||
else
|
||||
render json: {
|
||||
template: {
|
||||
name: result[:template_name],
|
||||
template_id: result[:template_id],
|
||||
status: 'PENDING',
|
||||
language: result[:language] || 'en'
|
||||
}
|
||||
}, status: :created
|
||||
end
|
||||
end
|
||||
|
||||
def render_failed_template_creation(result)
|
||||
@@ -98,45 +88,6 @@ class Api::V1::Accounts::InboxCsatTemplatesController < Api::V1::Accounts::BaseC
|
||||
}, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def delete_existing_template_if_needed
|
||||
template = @inbox.csat_config&.dig('template')
|
||||
return true if template.blank?
|
||||
|
||||
template_name = template['name']
|
||||
return true if template_name.blank?
|
||||
|
||||
template_status = @inbox.channel.provider_service.get_template_status(template_name)
|
||||
return true unless template_status[:success]
|
||||
|
||||
deletion_result = @inbox.channel.provider_service.delete_csat_template(template_name)
|
||||
if deletion_result[:success]
|
||||
Rails.logger.info "Deleted existing CSAT template '#{template_name}' for inbox #{@inbox.id}"
|
||||
true
|
||||
else
|
||||
Rails.logger.warn "Failed to delete existing CSAT template '#{template_name}' for inbox #{@inbox.id}: #{deletion_result[:response_body]}"
|
||||
false
|
||||
end
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Error during template deletion for inbox #{@inbox.id}: #{e.message}"
|
||||
false
|
||||
end
|
||||
|
||||
def render_template_status_response(status_result, template_name)
|
||||
if status_result[:success]
|
||||
render json: {
|
||||
template_exists: true,
|
||||
template_name: template_name,
|
||||
status: status_result[:template][:status],
|
||||
template_id: status_result[:template][:id]
|
||||
}
|
||||
else
|
||||
render json: {
|
||||
template_exists: false,
|
||||
error: 'Template not found'
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def parse_whatsapp_error(response_body)
|
||||
return { user_message: nil, technical_details: nil } if response_body.blank?
|
||||
|
||||
|
||||
@@ -176,7 +176,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
:lock_to_single_conversation, :portal_id, :sender_name_type, :business_name,
|
||||
{ csat_config: [:display_type, :message, :button_text, :language,
|
||||
{ survey_rules: [:operator, { values: [] }],
|
||||
template: [:name, :template_id, :created_at, :language] }] }]
|
||||
template: [:name, :template_id, :friendly_name, :content_sid, :approval_sid, :created_at, :language, :status] }] }]
|
||||
end
|
||||
|
||||
def permitted_params(channel_attributes = [])
|
||||
|
||||
@@ -8,6 +8,7 @@ class Api::V1::Accounts::Integrations::HooksController < Api::V1::Accounts::Base
|
||||
|
||||
def update
|
||||
@hook.update!(permitted_params.slice(:status, :settings))
|
||||
@hook.reauthorized! if @hook.reauthorization_required?
|
||||
end
|
||||
|
||||
def process_event
|
||||
@@ -18,7 +19,7 @@ class Api::V1::Accounts::Integrations::HooksController < Api::V1::Accounts::Base
|
||||
if response.nil?
|
||||
render json: { message: nil }
|
||||
elsif response[:error]
|
||||
render json: { error: response[:error] }, status: :unprocessable_entity
|
||||
render json: { error: response[:error], error_type: response[:error_type] }, status: :unprocessable_entity
|
||||
else
|
||||
render json: { message: response[:message] }
|
||||
end
|
||||
|
||||
@@ -38,6 +38,11 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||
generate_csv('teams_report', 'api/v2/accounts/reports/teams')
|
||||
end
|
||||
|
||||
def conversations_summary
|
||||
@report_data = generate_conversations_report
|
||||
generate_csv('conversations_summary_report', 'api/v2/accounts/reports/conversations_summary')
|
||||
end
|
||||
|
||||
def conversation_traffic
|
||||
@report_data = generate_conversations_heatmap_report
|
||||
timezone_offset = (params[:timezone_offset] || 0).to_f
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseController
|
||||
before_action :check_authorization
|
||||
before_action :prepare_builder_params, only: [:agent, :team, :inbox, :label]
|
||||
before_action :prepare_builder_params, only: [:agent, :team, :inbox, :label, :channel]
|
||||
|
||||
def agent
|
||||
render_report_with(V2::Reports::AgentSummaryBuilder)
|
||||
@@ -18,6 +18,12 @@ class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseContr
|
||||
render_report_with(V2::Reports::LabelSummaryBuilder)
|
||||
end
|
||||
|
||||
def channel
|
||||
return render_could_not_create_error(I18n.t('errors.reports.date_range_too_long')) if date_range_too_long?
|
||||
|
||||
render_report_with(V2::Reports::ChannelSummaryBuilder)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
@@ -40,4 +46,12 @@ class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseContr
|
||||
def permitted_params
|
||||
params.permit(:since, :until, :business_hours)
|
||||
end
|
||||
|
||||
def date_range_too_long?
|
||||
return false if permitted_params[:since].blank? || permitted_params[:until].blank?
|
||||
|
||||
since_time = Time.zone.at(permitted_params[:since].to_i)
|
||||
until_time = Time.zone.at(permitted_params[:until].to_i)
|
||||
(until_time - since_time) > 6.months
|
||||
end
|
||||
end
|
||||
|
||||
@@ -16,7 +16,7 @@ class DashboardController < ActionController::Base
|
||||
CHATWOOT_INBOX_TOKEN
|
||||
API_CHANNEL_NAME
|
||||
API_CHANNEL_THUMBNAIL
|
||||
ANALYTICS_TOKEN
|
||||
CLOUD_ANALYTICS_TOKEN
|
||||
DIRECT_UPLOADS_ENABLED
|
||||
MAXIMUM_FILE_UPLOAD_SIZE
|
||||
HCAPTCHA_SITE_KEY
|
||||
|
||||
@@ -46,6 +46,13 @@ module Api::V2::Accounts::ReportsHelper
|
||||
end
|
||||
end
|
||||
|
||||
def generate_conversations_report
|
||||
builder = V2::Reports::Conversations::MetricBuilder.new(Current.account, build_params(type: :account))
|
||||
summary = builder.summary
|
||||
|
||||
[generate_conversation_report_metrics(summary)]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_params(base_params)
|
||||
@@ -71,4 +78,16 @@ module Api::V2::Accounts::ReportsHelper
|
||||
report[:resolved_conversations_count]
|
||||
]
|
||||
end
|
||||
|
||||
def generate_conversation_report_metrics(summary)
|
||||
[
|
||||
summary[:conversations_count],
|
||||
summary[:incoming_messages_count],
|
||||
summary[:outgoing_messages_count],
|
||||
Reports::TimeFormatPresenter.new(summary[:avg_first_response_time]).format,
|
||||
Reports::TimeFormatPresenter.new(summary[:avg_resolution_time]).format,
|
||||
summary[:resolutions_count],
|
||||
Reports::TimeFormatPresenter.new(summary[:reply_time]).format
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/* global axios */
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
class CaptainPreferences extends ApiClient {
|
||||
constructor() {
|
||||
super('captain/preferences', { accountScoped: true });
|
||||
}
|
||||
|
||||
get() {
|
||||
return axios.get(this.url);
|
||||
}
|
||||
|
||||
updatePreferences(data) {
|
||||
return axios.put(this.url, data);
|
||||
}
|
||||
}
|
||||
|
||||
export default new CaptainPreferences();
|
||||
@@ -61,6 +61,12 @@ class ReportsAPI extends ApiClient {
|
||||
});
|
||||
}
|
||||
|
||||
getConversationsSummaryReports({ from: since, to: until, businessHours }) {
|
||||
return axios.get(`${this.url}/conversations_summary`, {
|
||||
params: { since, until, business_hours: businessHours },
|
||||
});
|
||||
}
|
||||
|
||||
getConversationTrafficCSV({ daysBefore = 6 } = {}) {
|
||||
return axios.get(`${this.url}/conversation_traffic`, {
|
||||
params: { timezone_offset: getTimeOffset(), days_before: daysBefore },
|
||||
|
||||
+4
@@ -35,6 +35,10 @@ const sortMenus = [
|
||||
label: t('COMPANIES.SORT_BY.OPTIONS.CREATED_AT'),
|
||||
value: 'created_at',
|
||||
},
|
||||
{
|
||||
label: t('COMPANIES.SORT_BY.OPTIONS.CONTACTS_COUNT'),
|
||||
value: 'contacts_count',
|
||||
},
|
||||
];
|
||||
|
||||
const orderingMenus = [
|
||||
|
||||
@@ -5,6 +5,7 @@ import WootEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: String, default: '' },
|
||||
editorKey: { type: String, default: '' },
|
||||
label: { type: String, default: '' },
|
||||
placeholder: { type: String, default: '' },
|
||||
focusOnMount: { type: Boolean, default: false },
|
||||
@@ -96,6 +97,7 @@ watch(
|
||||
]"
|
||||
>
|
||||
<WootEditor
|
||||
:editor-id="editorKey"
|
||||
:model-value="modelValue"
|
||||
:placeholder="placeholder"
|
||||
:focus-on-mount="focusOnMount"
|
||||
@@ -152,6 +154,13 @@ watch(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ProseMirror-menubar {
|
||||
width: fit-content !important;
|
||||
position: relative !important;
|
||||
top: unset !important;
|
||||
@apply ltr:left-[-0.188rem] rtl:right-[-0.188rem] !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-1
@@ -44,6 +44,12 @@ const emit = defineEmits([
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const attachmentId = ref(0);
|
||||
const generateUid = () => {
|
||||
attachmentId.value += 1;
|
||||
return `attachment-${attachmentId.value}`;
|
||||
};
|
||||
|
||||
const uploadAttachment = ref(null);
|
||||
const isEmojiPickerOpen = ref(false);
|
||||
|
||||
@@ -176,7 +182,8 @@ const onPaste = e => {
|
||||
.filter(file => file.size > 0)
|
||||
.forEach(file => {
|
||||
const { name, type, size } = file;
|
||||
onFileUpload({ file, name, type, size });
|
||||
// Add unique ID for clipboard-pasted files
|
||||
onFileUpload({ file, name, type, size, id: generateUid() });
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
+79
-61
@@ -7,6 +7,7 @@ import {
|
||||
appendSignature,
|
||||
removeSignature,
|
||||
getEffectiveChannelType,
|
||||
stripUnsupportedMarkdown,
|
||||
} from 'dashboard/helper/editorHelper';
|
||||
import {
|
||||
buildContactableInboxesList,
|
||||
@@ -47,6 +48,8 @@ const emit = defineEmits([
|
||||
'createConversation',
|
||||
]);
|
||||
|
||||
const DEFAULT_FORMATTING = 'Context::Default';
|
||||
|
||||
const showContactsDropdown = ref(false);
|
||||
const showInboxesDropdown = ref(false);
|
||||
const showCcEmailsDropdown = ref(false);
|
||||
@@ -198,10 +201,22 @@ const setSelectedContact = async ({ value, action, ...rest }) => {
|
||||
showInboxesDropdown.value = true;
|
||||
};
|
||||
|
||||
const handleInboxAction = ({ value, action, ...rest }) => {
|
||||
const stripMessageFormatting = channelType => {
|
||||
if (!state.message || !channelType) return;
|
||||
|
||||
state.message = stripUnsupportedMarkdown(state.message, channelType, false);
|
||||
};
|
||||
|
||||
const handleInboxAction = ({ value, action, channelType, medium, ...rest }) => {
|
||||
v$.value.$reset();
|
||||
state.message = '';
|
||||
emit('updateTargetInbox', { ...rest });
|
||||
|
||||
// Strip unsupported formatting when changing the target inbox
|
||||
if (channelType) {
|
||||
const newChannelType = getEffectiveChannelType(channelType, medium);
|
||||
stripMessageFormatting(newChannelType);
|
||||
}
|
||||
|
||||
emit('updateTargetInbox', { ...rest, channelType, medium });
|
||||
showInboxesDropdown.value = false;
|
||||
state.attachedFiles = [];
|
||||
};
|
||||
@@ -221,7 +236,9 @@ const removeSignatureFromMessage = () => {
|
||||
const removeTargetInbox = value => {
|
||||
v$.value.$reset();
|
||||
removeSignatureFromMessage();
|
||||
state.message = '';
|
||||
|
||||
stripMessageFormatting(DEFAULT_FORMATTING);
|
||||
|
||||
emit('updateTargetInbox', value);
|
||||
state.attachedFiles = [];
|
||||
};
|
||||
@@ -324,67 +341,68 @@ const shouldShowMessageEditor = computed(() => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="w-[42rem] divide-y divide-n-strong overflow-visible transition-all duration-300 ease-in-out top-full justify-between flex flex-col bg-n-alpha-3 border border-n-strong shadow-sm backdrop-blur-[100px] rounded-xl min-w-0"
|
||||
class="w-[42rem] divide-y divide-n-strong overflow-visible transition-all duration-300 ease-in-out top-full flex flex-col bg-n-alpha-3 border border-n-strong shadow-sm backdrop-blur-[100px] rounded-xl min-w-0 max-h-[calc(100vh-8rem)]"
|
||||
>
|
||||
<ContactSelector
|
||||
:contacts="contacts"
|
||||
:selected-contact="selectedContact"
|
||||
:show-contacts-dropdown="showContactsDropdown"
|
||||
:is-loading="isLoading"
|
||||
:is-creating-contact="isCreatingContact"
|
||||
:contact-id="contactId"
|
||||
:contactable-inboxes-list="contactableInboxesList"
|
||||
:show-inboxes-dropdown="showInboxesDropdown"
|
||||
:has-errors="validationStates.isContactInvalid"
|
||||
@search-contacts="handleContactSearch"
|
||||
@set-selected-contact="setSelectedContact"
|
||||
@clear-selected-contact="clearSelectedContact"
|
||||
@update-dropdown="handleDropdownUpdate"
|
||||
/>
|
||||
<InboxEmptyState v-if="showNoInboxAlert" />
|
||||
<InboxSelector
|
||||
v-else
|
||||
:target-inbox="targetInbox"
|
||||
:selected-contact="selectedContact"
|
||||
:show-inboxes-dropdown="showInboxesDropdown"
|
||||
:contactable-inboxes-list="contactableInboxesList"
|
||||
:has-errors="validationStates.isInboxInvalid"
|
||||
@update-inbox="removeTargetInbox"
|
||||
@toggle-dropdown="showInboxesDropdown = $event"
|
||||
@handle-inbox-action="handleInboxAction"
|
||||
/>
|
||||
<div class="flex-1 overflow-y-auto divide-y divide-n-strong">
|
||||
<ContactSelector
|
||||
:contacts="contacts"
|
||||
:selected-contact="selectedContact"
|
||||
:show-contacts-dropdown="showContactsDropdown"
|
||||
:is-loading="isLoading"
|
||||
:is-creating-contact="isCreatingContact"
|
||||
:contact-id="contactId"
|
||||
:contactable-inboxes-list="contactableInboxesList"
|
||||
:show-inboxes-dropdown="showInboxesDropdown"
|
||||
:has-errors="validationStates.isContactInvalid"
|
||||
@search-contacts="handleContactSearch"
|
||||
@set-selected-contact="setSelectedContact"
|
||||
@clear-selected-contact="clearSelectedContact"
|
||||
@update-dropdown="handleDropdownUpdate"
|
||||
/>
|
||||
<InboxEmptyState v-if="showNoInboxAlert" />
|
||||
<InboxSelector
|
||||
v-else
|
||||
:target-inbox="targetInbox"
|
||||
:selected-contact="selectedContact"
|
||||
:show-inboxes-dropdown="showInboxesDropdown"
|
||||
:contactable-inboxes-list="contactableInboxesList"
|
||||
:has-errors="validationStates.isInboxInvalid"
|
||||
@update-inbox="removeTargetInbox"
|
||||
@toggle-dropdown="showInboxesDropdown = $event"
|
||||
@handle-inbox-action="handleInboxAction"
|
||||
/>
|
||||
|
||||
<EmailOptions
|
||||
v-if="inboxTypes.isEmail"
|
||||
v-model:cc-emails="state.ccEmails"
|
||||
v-model:bcc-emails="state.bccEmails"
|
||||
v-model:subject="state.subject"
|
||||
:contacts="contacts"
|
||||
:show-cc-emails-dropdown="showCcEmailsDropdown"
|
||||
:show-bcc-emails-dropdown="showBccEmailsDropdown"
|
||||
:is-loading="isLoading"
|
||||
:has-errors="validationStates.isSubjectInvalid"
|
||||
@search-cc-emails="searchCcEmails"
|
||||
@search-bcc-emails="searchBccEmails"
|
||||
@update-dropdown="handleDropdownUpdate"
|
||||
/>
|
||||
<EmailOptions
|
||||
v-if="inboxTypes.isEmail"
|
||||
v-model:cc-emails="state.ccEmails"
|
||||
v-model:bcc-emails="state.bccEmails"
|
||||
v-model:subject="state.subject"
|
||||
:contacts="contacts"
|
||||
:show-cc-emails-dropdown="showCcEmailsDropdown"
|
||||
:show-bcc-emails-dropdown="showBccEmailsDropdown"
|
||||
:is-loading="isLoading"
|
||||
:has-errors="validationStates.isSubjectInvalid"
|
||||
@search-cc-emails="searchCcEmails"
|
||||
@search-bcc-emails="searchBccEmails"
|
||||
@update-dropdown="handleDropdownUpdate"
|
||||
/>
|
||||
|
||||
<MessageEditor
|
||||
v-if="shouldShowMessageEditor"
|
||||
v-model="state.message"
|
||||
:message-signature="messageSignature"
|
||||
:send-with-signature="sendWithSignature"
|
||||
:has-errors="validationStates.isMessageInvalid"
|
||||
:has-attachments="state.attachedFiles.length > 0"
|
||||
:channel-type="inboxChannelType"
|
||||
:medium="targetInbox?.medium || ''"
|
||||
/>
|
||||
<MessageEditor
|
||||
v-if="shouldShowMessageEditor"
|
||||
v-model="state.message"
|
||||
:message-signature="messageSignature"
|
||||
:send-with-signature="sendWithSignature"
|
||||
:has-errors="validationStates.isMessageInvalid"
|
||||
:channel-type="inboxChannelType"
|
||||
:medium="targetInbox?.medium || ''"
|
||||
/>
|
||||
|
||||
<AttachmentPreviews
|
||||
v-if="state.attachedFiles.length > 0"
|
||||
:attachments="state.attachedFiles"
|
||||
@update:attachments="state.attachedFiles = $event"
|
||||
/>
|
||||
<AttachmentPreviews
|
||||
v-if="state.attachedFiles.length > 0"
|
||||
:attachments="state.attachedFiles"
|
||||
@update:attachments="state.attachedFiles = $event"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ActionButtons
|
||||
:attached-files="state.attachedFiles"
|
||||
|
||||
+1
-1
@@ -83,7 +83,7 @@ const targetInboxLabel = computed(() => {
|
||||
<DropdownMenu
|
||||
v-if="contactableInboxesList?.length > 0 && showInboxesDropdown"
|
||||
:menu-items="contactableInboxesList"
|
||||
class="ltr:left-0 rtl:right-0 z-[100] top-8 overflow-y-auto max-h-60 w-fit max-w-sm dark:!outline-n-slate-5"
|
||||
class="ltr:left-0 rtl:right-0 z-[100] top-8 overflow-y-auto max-h-56 w-fit max-w-sm dark:!outline-n-slate-5"
|
||||
@action="emit('handleInboxAction', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
+3
-4
@@ -6,7 +6,6 @@ import Editor from 'dashboard/components-next/Editor/Editor.vue';
|
||||
|
||||
const props = defineProps({
|
||||
hasErrors: { type: Boolean, default: false },
|
||||
hasAttachments: { type: Boolean, default: false },
|
||||
sendWithSignature: { type: Boolean, default: false },
|
||||
messageSignature: { type: String, default: '' },
|
||||
channelType: { type: String, default: '' },
|
||||
@@ -24,14 +23,14 @@ const modelValue = defineModel({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-1 h-full" :class="[!hasAttachments && 'min-h-[200px]']">
|
||||
<div class="flex-1 h-full">
|
||||
<Editor
|
||||
:key="editorKey"
|
||||
v-model="modelValue"
|
||||
:editor-key="editorKey"
|
||||
:placeholder="
|
||||
t('COMPOSE_NEW_CONVERSATION.FORM.MESSAGE_EDITOR.PLACEHOLDER')
|
||||
"
|
||||
class="[&>div]:!border-transparent [&>div]:px-4 [&>div]:py-4 [&>div]:!bg-transparent h-full [&_.ProseMirror-woot-style]:!max-h-[200px]"
|
||||
class="[&>div]:!border-transparent [&>div]:px-4 [&>div]:py-4 [&>div]:!bg-transparent h-full [&_.ProseMirror-woot-style]:!max-h-[12.5rem] [&_.ProseMirror-woot-style]:!min-h-[10rem] [&_.ProseMirror-menubar]:!pt-0 [&_.mention--box]:-top-[7.5rem] [&_.mention--box]:bottom-[unset]"
|
||||
:class="
|
||||
hasErrors
|
||||
? '[&_.empty-node]:before:!text-n-ruby-9 [&_.empty-node]:dark:before:!text-n-ruby-9'
|
||||
|
||||
@@ -11,7 +11,6 @@ export const CONVERSATION_ATTRIBUTES = {
|
||||
CAMPAIGN_ID: 'campaign_id',
|
||||
LABELS: 'labels',
|
||||
BROWSER_LANGUAGE: 'browser_language',
|
||||
COUNTRY_CODE: 'country_code',
|
||||
REFERER: 'referer',
|
||||
CREATED_AT: 'created_at',
|
||||
LAST_ACTIVITY_AT: 'last_activity_at',
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
buildAttributesFilterTypes,
|
||||
CONVERSATION_ATTRIBUTES,
|
||||
} from './helper/filterHelper';
|
||||
import countries from 'shared/constants/countries.js';
|
||||
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages.js';
|
||||
|
||||
/**
|
||||
@@ -218,17 +217,6 @@ export function useConversationFilterContext() {
|
||||
filterOperators: equalityOperators.value,
|
||||
attributeModel: 'additional',
|
||||
},
|
||||
{
|
||||
attributeKey: CONVERSATION_ATTRIBUTES.COUNTRY_CODE,
|
||||
value: CONVERSATION_ATTRIBUTES.COUNTRY_CODE,
|
||||
attributeName: t('FILTER.ATTRIBUTES.COUNTRY_NAME'),
|
||||
label: t('FILTER.ATTRIBUTES.COUNTRY_NAME'),
|
||||
inputType: 'searchSelect',
|
||||
options: countries,
|
||||
dataType: 'text',
|
||||
filterOperators: equalityOperators.value,
|
||||
attributeModel: 'additional',
|
||||
},
|
||||
{
|
||||
attributeKey: CONVERSATION_ATTRIBUTES.REFERER,
|
||||
value: CONVERSATION_ATTRIBUTES.REFERER,
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<script setup>
|
||||
import { h, computed, onMounted } from 'vue';
|
||||
import { h, ref, computed, onMounted } from 'vue';
|
||||
import { provideSidebarContext } from './provider';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { useKbd } from 'dashboard/composables/utils/useKbd';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { useStore } from 'vuex';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStorage } from '@vueuse/core';
|
||||
import { useSidebarKeyboardShortcuts } from './useSidebarKeyboardShortcuts';
|
||||
import { vOnClickOutside } from '@vueuse/components';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
@@ -16,7 +15,6 @@ import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import SidebarGroup from './SidebarGroup.vue';
|
||||
import SidebarProfileMenu from './SidebarProfileMenu.vue';
|
||||
import SidebarChangelogCard from './SidebarChangelogCard.vue';
|
||||
import YearInReviewBanner from '../year-in-review/YearInReviewBanner.vue';
|
||||
import ChannelLeaf from './ChannelLeaf.vue';
|
||||
import SidebarAccountSwitcher from './SidebarAccountSwitcher.vue';
|
||||
import Logo from 'next/icon/Logo.vue';
|
||||
@@ -55,14 +53,7 @@ const toggleShortcutModalFn = show => {
|
||||
|
||||
useSidebarKeyboardShortcuts(toggleShortcutModalFn);
|
||||
|
||||
// We're using localStorage to store the expanded item in the sidebar
|
||||
// This helps preserve context when navigating between portal and dashboard layouts
|
||||
// and also when the user refreshes the page
|
||||
const expandedItem = useStorage(
|
||||
'next-sidebar-expanded-item',
|
||||
null,
|
||||
sessionStorage
|
||||
);
|
||||
const expandedItem = ref(null);
|
||||
|
||||
const setExpandedItem = name => {
|
||||
expandedItem.value = expandedItem.value === name ? null : name;
|
||||
@@ -493,6 +484,12 @@ const menuItems = computed(() => {
|
||||
icon: 'i-lucide-briefcase',
|
||||
to: accountScopedRoute('general_settings_index'),
|
||||
},
|
||||
// {
|
||||
// name: 'Settings Captain',
|
||||
// label: t('SIDEBAR.CAPTAIN_AI'),
|
||||
// icon: 'i-woot-captain',
|
||||
// to: accountScopedRoute('captain_settings_index'),
|
||||
// },
|
||||
{
|
||||
name: 'Settings Agents',
|
||||
label: t('SIDEBAR.AGENTS'),
|
||||
@@ -663,7 +660,6 @@ const menuItems = computed(() => {
|
||||
<div
|
||||
class="pointer-events-none absolute inset-x-0 -top-[31px] h-8 bg-gradient-to-t from-n-solid-2 to-transparent"
|
||||
/>
|
||||
<YearInReviewBanner />
|
||||
<SidebarChangelogCard
|
||||
v-if="isOnChatwootCloud && !isACustomBrandedInstance"
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, nextTick } from 'vue';
|
||||
import { computed, onMounted, watch, nextTick } from 'vue';
|
||||
import { useSidebarContext } from './provider';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import Policy from 'dashboard/components/policy.vue';
|
||||
@@ -126,6 +126,16 @@ onMounted(async () => {
|
||||
setExpandedItem(props.name);
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
hasActiveChild,
|
||||
hasNewActiveChild => {
|
||||
if (hasNewActiveChild && !isExpanded.value) {
|
||||
setExpandedItem(props.name);
|
||||
}
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import Auth from 'dashboard/api/auth';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import Avatar from 'next/avatar/Avatar.vue';
|
||||
import SidebarProfileMenuStatus from './SidebarProfileMenuStatus.vue';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
import YearInReviewModal from 'dashboard/components-next/year-in-review/YearInReviewModal.vue';
|
||||
|
||||
import {
|
||||
DropdownContainer,
|
||||
@@ -24,7 +22,6 @@ defineOptions({
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
const { uiSettings } = useUISettings();
|
||||
|
||||
const currentUser = useMapGetter('getCurrentUser');
|
||||
const currentUserAvailability = useMapGetter('getCurrentUserAvailability');
|
||||
@@ -34,29 +31,6 @@ const isFeatureEnabledonAccount = useMapGetter(
|
||||
'accounts/isFeatureEnabledonAccount'
|
||||
);
|
||||
|
||||
const showYearInReviewModal = ref(false);
|
||||
|
||||
const bannerClosedKey = computed(() => {
|
||||
return `yir_closed_${accountId.value}_2025`;
|
||||
});
|
||||
|
||||
const isBannerClosed = computed(() => {
|
||||
return uiSettings.value?.[bannerClosedKey.value] === true;
|
||||
});
|
||||
|
||||
const showYearInReviewMenuItem = computed(() => {
|
||||
return isBannerClosed.value;
|
||||
});
|
||||
|
||||
const openYearInReviewModal = () => {
|
||||
showYearInReviewModal.value = true;
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const closeYearInReviewModal = () => {
|
||||
showYearInReviewModal.value = false;
|
||||
};
|
||||
|
||||
const showChatSupport = computed(() => {
|
||||
return (
|
||||
isFeatureEnabledonAccount.value(
|
||||
@@ -68,13 +42,6 @@ const showChatSupport = computed(() => {
|
||||
|
||||
const menuItems = computed(() => {
|
||||
return [
|
||||
{
|
||||
show: showYearInReviewMenuItem.value,
|
||||
showOnCustomBrandedInstance: false,
|
||||
label: t('SIDEBAR_ITEMS.YEAR_IN_REVIEW'),
|
||||
icon: 'i-lucide-gift',
|
||||
click: openYearInReviewModal,
|
||||
},
|
||||
{
|
||||
show: showChatSupport.value,
|
||||
showOnCustomBrandedInstance: false,
|
||||
@@ -190,9 +157,4 @@ const allowedMenuItems = computed(() => {
|
||||
</template>
|
||||
</DropdownBody>
|
||||
</DropdownContainer>
|
||||
|
||||
<YearInReviewModal
|
||||
:show="showYearInReviewModal"
|
||||
@close="closeYearInReviewModal"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -230,7 +230,7 @@ const handleBlur = e => emit('blur', e);
|
||||
v-if="showDropdownMenu"
|
||||
:menu-items="filteredMenuItems"
|
||||
:is-searching="isLoading"
|
||||
class="ltr:left-0 rtl:right-0 z-[100] top-8 overflow-y-auto max-h-60 w-[inherit] max-w-md dark:!outline-n-slate-5"
|
||||
class="ltr:left-0 rtl:right-0 z-[100] top-8 overflow-y-auto max-h-56 w-[inherit] max-w-md dark:!outline-n-slate-5"
|
||||
@action="handleDropdownAction"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -206,10 +206,14 @@ const emitDateRange = () => {
|
||||
emit('dateRangeChanged', [selectedStartDate.value, selectedEndDate.value]);
|
||||
}
|
||||
};
|
||||
|
||||
const closeDatePicker = () => {
|
||||
showDatePicker.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative font-inter">
|
||||
<div v-on-clickaway="closeDatePicker" class="relative font-inter">
|
||||
<DatePickerButton
|
||||
:selected-start-date="selectedStartDate"
|
||||
:selected-end-date="selectedEndDate"
|
||||
|
||||
@@ -38,7 +38,7 @@ const toggleShowMore = () => {
|
||||
<button
|
||||
v-if="text.length > limit"
|
||||
class="text-n-brand !p-0 !border-0 align-top"
|
||||
@click="toggleShowMore"
|
||||
@click.stop="toggleShowMore"
|
||||
>
|
||||
{{ buttonLabel }}
|
||||
</button>
|
||||
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
removeSignature,
|
||||
getEffectiveChannelType,
|
||||
} from 'dashboard/helper/editorHelper';
|
||||
import { isFileTypeAllowedForChannel } from 'shared/helpers/FileHelper';
|
||||
|
||||
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||
@@ -637,6 +638,25 @@ export default {
|
||||
// Filter valid files (non-zero size)
|
||||
Array.from(e.clipboardData.files)
|
||||
.filter(file => file.size > 0)
|
||||
.filter(file => {
|
||||
const isAllowed = isFileTypeAllowedForChannel(file, {
|
||||
channelType: this.channelType || this.inbox?.channel_type,
|
||||
medium: this.inbox?.medium,
|
||||
conversationType: this.conversationType,
|
||||
isInstagramChannel: this.isAnInstagramChannel,
|
||||
isOnPrivateNote: this.isOnPrivateNote,
|
||||
});
|
||||
|
||||
if (!isAllowed) {
|
||||
useAlert(
|
||||
this.$t('CONVERSATION.FILE_TYPE_NOT_SUPPORTED', {
|
||||
fileName: file.name,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return isAllowed;
|
||||
})
|
||||
.forEach(file => {
|
||||
const { name, type, size } = file;
|
||||
this.onFileUpload({ name, type, size, file });
|
||||
|
||||
@@ -78,14 +78,6 @@ const filterTypes = [
|
||||
filterOperators: OPERATOR_TYPES_1,
|
||||
attributeModel: 'additional',
|
||||
},
|
||||
{
|
||||
attributeKey: 'country_code',
|
||||
attributeI18nKey: 'COUNTRY_NAME',
|
||||
inputType: 'search_select',
|
||||
dataType: 'text',
|
||||
filterOperators: OPERATOR_TYPES_1,
|
||||
attributeModel: 'additional',
|
||||
},
|
||||
{
|
||||
attributeKey: 'referer',
|
||||
attributeI18nKey: 'REFERER_LINK',
|
||||
@@ -171,10 +163,6 @@ export const filterAttributeGroups = [
|
||||
key: 'browser_language',
|
||||
i18nKey: 'BROWSER_LANGUAGE',
|
||||
},
|
||||
{
|
||||
key: 'country_code',
|
||||
i18nKey: 'COUNTRY_NAME',
|
||||
},
|
||||
{
|
||||
key: 'referer',
|
||||
i18nKey: 'REFERER_LINK',
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useAlert, useTrack } from 'dashboard/composables';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { OPEN_AI_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import OpenAPI from 'dashboard/api/integrations/openapi';
|
||||
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||
|
||||
/**
|
||||
* Cleans and normalizes a list of labels.
|
||||
@@ -155,6 +156,8 @@ export function useAI() {
|
||||
}
|
||||
};
|
||||
|
||||
const accountId = computed(() => getters.getCurrentAccountId.value);
|
||||
|
||||
/**
|
||||
* Processes an AI event, such as rephrasing content.
|
||||
* @param {string} [type='rephrase'] - The type of AI event to process.
|
||||
@@ -173,11 +176,29 @@ export function useAI() {
|
||||
} = result;
|
||||
return generatedMessage;
|
||||
} catch (error) {
|
||||
const errorData = error.response.data.error;
|
||||
const errorData = error.response?.data;
|
||||
const errorMessage =
|
||||
errorData?.error?.message ||
|
||||
errorData?.error ||
|
||||
t('INTEGRATION_SETTINGS.OPEN_AI.GENERATE_ERROR');
|
||||
useAlert(errorMessage);
|
||||
|
||||
const errorType = errorData?.error_type;
|
||||
if (errorType === 'rate_limit') {
|
||||
useAlert(t('INTEGRATION_SETTINGS.OPEN_AI.RATE_LIMIT_ERROR'), {
|
||||
duration: 5000,
|
||||
});
|
||||
} else if (errorType === 'auth') {
|
||||
useAlert(t('INTEGRATION_SETTINGS.OPEN_AI.AUTH_ERROR'), {
|
||||
type: 'link',
|
||||
to: frontendURL(
|
||||
`accounts/${accountId.value}/settings/integrations/openai`
|
||||
),
|
||||
message: t('INTEGRATION_SETTINGS.OPEN_AI.GO_TO_SETTINGS'),
|
||||
duration: 5000,
|
||||
});
|
||||
} else {
|
||||
useAlert(errorMessage);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -57,6 +57,14 @@ export const useIntegrationHook = integrationId => {
|
||||
return integrationType.value === 'single';
|
||||
});
|
||||
|
||||
/**
|
||||
* Whether any hook needs reauthorization (e.g., API key issues)
|
||||
* @type {import('vue').ComputedRef<boolean>}
|
||||
*/
|
||||
const hookNeedsReauthorization = computed(() => {
|
||||
return integration.value.hooks?.some(hook => hook.reauthorization_required);
|
||||
});
|
||||
|
||||
return {
|
||||
integration,
|
||||
integrationType,
|
||||
@@ -64,5 +72,6 @@ export const useIntegrationHook = integrationId => {
|
||||
isIntegrationSingle,
|
||||
isHookTypeInbox,
|
||||
hasConnectedHooks,
|
||||
hookNeedsReauthorization,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -237,10 +237,11 @@ export const MARKDOWN_PATTERNS = [
|
||||
patterns: [{ pattern: /`([^`]+)`/g, replacement: '$1' }],
|
||||
},
|
||||
{
|
||||
type: 'link', // PM: link, eg: [text](url) or <url>
|
||||
type: 'link', // PM: link
|
||||
patterns: [
|
||||
{ pattern: /\[([^\]]+)\]\([^)]+\)/g, replacement: '$1' }, // [text](url) -> text
|
||||
{ pattern: /<(https?:\/\/[^>]+)>/g, replacement: '$1' }, // <url> -> url (autolinks)
|
||||
{ pattern: /<([a-zA-Z][a-zA-Z0-9+.-]*:[^\s>]+)>/g, replacement: '$1' }, // <https://...>, <mailto:...>, <tel:...>, <ftp://...>, etc
|
||||
{ pattern: /<([^\s@]+@[^\s@>]+)>/g, replacement: '$1' }, // <user@example.com> -> user@example.com
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import posthog from 'posthog-js';
|
||||
import * as amplitude from '@amplitude/analytics-browser';
|
||||
|
||||
/**
|
||||
* AnalyticsHelper class to initialize and track user analytics
|
||||
@@ -26,12 +26,10 @@ export class AnalyticsHelper {
|
||||
return;
|
||||
}
|
||||
|
||||
posthog.init(this.analyticsToken, {
|
||||
api_host: 'https://app.posthog.com',
|
||||
capture_pageview: false,
|
||||
persistence: 'localStorage+cookie',
|
||||
amplitude.init(this.analyticsToken, {
|
||||
defaultTracking: false,
|
||||
});
|
||||
this.analytics = posthog;
|
||||
this.analytics = amplitude;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,20 +43,26 @@ export class AnalyticsHelper {
|
||||
}
|
||||
|
||||
this.user = user;
|
||||
this.analytics.identify(this.user.id.toString(), {
|
||||
email: this.user.email,
|
||||
name: this.user.name,
|
||||
avatar: this.user.avatar_url,
|
||||
});
|
||||
this.analytics.setUserId(`user-${this.user.id.toString()}`);
|
||||
|
||||
const identifyEvent = new amplitude.Identify();
|
||||
identifyEvent.set('email', this.user.email);
|
||||
identifyEvent.set('name', this.user.name);
|
||||
identifyEvent.set('avatar', this.user.avatar_url);
|
||||
this.analytics.identify(identifyEvent);
|
||||
|
||||
const { accounts, account_id: accountId } = this.user;
|
||||
const [currentAccount] = accounts.filter(
|
||||
account => account.id === accountId
|
||||
);
|
||||
if (currentAccount) {
|
||||
this.analytics.group('company', currentAccount.id.toString(), {
|
||||
name: currentAccount.name,
|
||||
});
|
||||
const groupId = `account-${currentAccount.id.toString()}`;
|
||||
|
||||
this.analytics.setGroup('company', groupId);
|
||||
|
||||
const groupIdentify = new amplitude.Identify();
|
||||
groupIdentify.set('name', currentAccount.name);
|
||||
this.analytics.groupIdentify('company', groupId, groupIdentify);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,20 +76,21 @@ export class AnalyticsHelper {
|
||||
if (!this.analytics) {
|
||||
return;
|
||||
}
|
||||
this.analytics.capture(eventName, properties);
|
||||
this.analytics.track(eventName, properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* Track the page views
|
||||
* @function
|
||||
* @param {Object} params - Page view properties
|
||||
* @param {string} pageName - Page name
|
||||
* @param {Object} [properties={}] - Page view properties
|
||||
*/
|
||||
page(params) {
|
||||
page(pageName, properties = {}) {
|
||||
if (!this.analytics) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.analytics.capture('$pageview', params);
|
||||
this.analytics.track('$pageview', { pageName, ...properties });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import helperObject, { AnalyticsHelper } from '../';
|
||||
|
||||
vi.mock('posthog-js', () => ({
|
||||
default: {
|
||||
init: vi.fn(),
|
||||
identify: vi.fn(),
|
||||
capture: vi.fn(),
|
||||
group: vi.fn(),
|
||||
},
|
||||
vi.mock('@amplitude/analytics-browser', () => ({
|
||||
init: vi.fn(),
|
||||
setUserId: vi.fn(),
|
||||
identify: vi.fn(),
|
||||
setGroup: vi.fn(),
|
||||
groupIdentify: vi.fn(),
|
||||
track: vi.fn(),
|
||||
Identify: vi.fn(() => ({
|
||||
set: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('helperObject', () => {
|
||||
@@ -22,12 +25,12 @@ describe('AnalyticsHelper', () => {
|
||||
});
|
||||
|
||||
describe('init', () => {
|
||||
it('should initialize posthog with the correct token', async () => {
|
||||
it('should initialize amplitude with the correct token', async () => {
|
||||
await analyticsHelper.init();
|
||||
expect(analyticsHelper.analytics).not.toBe(null);
|
||||
});
|
||||
|
||||
it('should not initialize posthog if token is not provided', async () => {
|
||||
it('should not initialize amplitude if token is not provided', async () => {
|
||||
analyticsHelper = new AnalyticsHelper();
|
||||
await analyticsHelper.init();
|
||||
expect(analyticsHelper.analytics).toBe(null);
|
||||
@@ -36,10 +39,15 @@ describe('AnalyticsHelper', () => {
|
||||
|
||||
describe('identify', () => {
|
||||
beforeEach(() => {
|
||||
analyticsHelper.analytics = { identify: vi.fn(), group: vi.fn() };
|
||||
analyticsHelper.analytics = {
|
||||
setUserId: vi.fn(),
|
||||
identify: vi.fn(),
|
||||
setGroup: vi.fn(),
|
||||
groupIdentify: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
it('should call identify on posthog with correct arguments', () => {
|
||||
it('should call setUserId and identify on amplitude with correct arguments', () => {
|
||||
analyticsHelper.identify({
|
||||
id: 123,
|
||||
email: 'test@example.com',
|
||||
@@ -49,19 +57,18 @@ describe('AnalyticsHelper', () => {
|
||||
account_id: 1,
|
||||
});
|
||||
|
||||
expect(analyticsHelper.analytics.identify).toHaveBeenCalledWith('123', {
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
avatar: 'avatar_url',
|
||||
});
|
||||
expect(analyticsHelper.analytics.group).toHaveBeenCalledWith(
|
||||
'company',
|
||||
'1',
|
||||
{ name: 'Account 1' }
|
||||
expect(analyticsHelper.analytics.setUserId).toHaveBeenCalledWith(
|
||||
'user-123'
|
||||
);
|
||||
expect(analyticsHelper.analytics.identify).toHaveBeenCalled();
|
||||
expect(analyticsHelper.analytics.setGroup).toHaveBeenCalledWith(
|
||||
'company',
|
||||
'account-1'
|
||||
);
|
||||
expect(analyticsHelper.analytics.groupIdentify).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call identify on posthog without group', () => {
|
||||
it('should call identify on amplitude without group', () => {
|
||||
analyticsHelper.identify({
|
||||
id: 123,
|
||||
email: 'test@example.com',
|
||||
@@ -71,10 +78,10 @@ describe('AnalyticsHelper', () => {
|
||||
account_id: 5,
|
||||
});
|
||||
|
||||
expect(analyticsHelper.analytics.group).not.toHaveBeenCalled();
|
||||
expect(analyticsHelper.analytics.setGroup).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not call analytics.page if analytics is null', () => {
|
||||
it('should not call analytics methods if analytics is null', () => {
|
||||
analyticsHelper.analytics = null;
|
||||
analyticsHelper.identify({});
|
||||
expect(analyticsHelper.analytics).toBe(null);
|
||||
@@ -83,27 +90,27 @@ describe('AnalyticsHelper', () => {
|
||||
|
||||
describe('track', () => {
|
||||
beforeEach(() => {
|
||||
analyticsHelper.analytics = { capture: vi.fn() };
|
||||
analyticsHelper.analytics = { track: vi.fn() };
|
||||
analyticsHelper.user = { id: 123 };
|
||||
});
|
||||
|
||||
it('should call capture on posthog with correct arguments', () => {
|
||||
it('should call track on amplitude with correct arguments', () => {
|
||||
analyticsHelper.track('Test Event', { prop1: 'value1', prop2: 'value2' });
|
||||
expect(analyticsHelper.analytics.capture).toHaveBeenCalledWith(
|
||||
expect(analyticsHelper.analytics.track).toHaveBeenCalledWith(
|
||||
'Test Event',
|
||||
{ prop1: 'value1', prop2: 'value2' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should call capture on posthog with default properties', () => {
|
||||
it('should call track on amplitude with default properties', () => {
|
||||
analyticsHelper.track('Test Event');
|
||||
expect(analyticsHelper.analytics.capture).toHaveBeenCalledWith(
|
||||
expect(analyticsHelper.analytics.track).toHaveBeenCalledWith(
|
||||
'Test Event',
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should not call capture on posthog if analytics is not initialized', () => {
|
||||
it('should not call track on amplitude if analytics is not initialized', () => {
|
||||
analyticsHelper.analytics = null;
|
||||
analyticsHelper.track('Test Event', { prop1: 'value1', prop2: 'value2' });
|
||||
expect(analyticsHelper.analytics).toBe(null);
|
||||
@@ -112,24 +119,25 @@ describe('AnalyticsHelper', () => {
|
||||
|
||||
describe('page', () => {
|
||||
beforeEach(() => {
|
||||
analyticsHelper.analytics = { capture: vi.fn() };
|
||||
analyticsHelper.analytics = { track: vi.fn() };
|
||||
});
|
||||
|
||||
it('should call the capture method for pageview with the correct arguments', () => {
|
||||
const params = {
|
||||
name: 'Test page',
|
||||
url: '/test',
|
||||
it('should call the track method for pageview with the correct arguments', () => {
|
||||
const pageName = 'home';
|
||||
const properties = {
|
||||
path: '/test',
|
||||
name: 'home',
|
||||
};
|
||||
analyticsHelper.page(params);
|
||||
expect(analyticsHelper.analytics.capture).toHaveBeenCalledWith(
|
||||
analyticsHelper.page(pageName, properties);
|
||||
expect(analyticsHelper.analytics.track).toHaveBeenCalledWith(
|
||||
'$pageview',
|
||||
params
|
||||
{ pageName: 'home', path: '/test', name: 'home' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should not call analytics.capture if analytics is null', () => {
|
||||
it('should not call analytics.track if analytics is null', () => {
|
||||
analyticsHelper.analytics = null;
|
||||
analyticsHelper.page();
|
||||
analyticsHelper.page('home');
|
||||
expect(analyticsHelper.analytics).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,9 +38,14 @@ export function extractTextFromMarkdown(markdown) {
|
||||
*
|
||||
* @param {string} markdown - markdown text to process
|
||||
* @param {string} channelType - The channel type to check supported formatting
|
||||
* @param {boolean} cleanWhitespace - Whether to clean up extra whitespace and blank lines (default: true for signatures)
|
||||
* @returns {string} - The markdown with unsupported formatting removed
|
||||
*/
|
||||
export function stripUnsupportedSignatureMarkdown(markdown, channelType) {
|
||||
export function stripUnsupportedMarkdown(
|
||||
markdown,
|
||||
channelType,
|
||||
cleanWhitespace = true
|
||||
) {
|
||||
if (!markdown) return '';
|
||||
|
||||
const { marks = [], nodes = [] } = FORMATTING[channelType] || {};
|
||||
@@ -55,6 +60,9 @@ export function stripUnsupportedSignatureMarkdown(markdown, channelType) {
|
||||
);
|
||||
}, markdown);
|
||||
|
||||
if (!cleanWhitespace) return result;
|
||||
|
||||
// Clean whitespace for signatures
|
||||
return result
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
@@ -155,7 +163,7 @@ export function getEffectiveChannelType(channelType, medium) {
|
||||
export function appendSignature(body, signature, channelType) {
|
||||
// Strip only unsupported formatting based on channel capabilities
|
||||
const preparedSignature = channelType
|
||||
? stripUnsupportedSignatureMarkdown(signature, channelType)
|
||||
? stripUnsupportedMarkdown(signature, channelType)
|
||||
: signature;
|
||||
const cleanedSignature = cleanSignature(preparedSignature);
|
||||
// if signature is already present, return body
|
||||
@@ -178,7 +186,7 @@ export function appendSignature(body, signature, channelType) {
|
||||
export function removeSignature(body, signature, channelType) {
|
||||
// Build unique list of signature variants to try
|
||||
const channelStripped = channelType
|
||||
? cleanSignature(stripUnsupportedSignatureMarkdown(signature, channelType))
|
||||
? cleanSignature(stripUnsupportedMarkdown(signature, channelType))
|
||||
: null;
|
||||
const signaturesToTry = [
|
||||
cleanSignature(signature),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
// Quote detection strategies
|
||||
const QUOTE_INDICATORS = [
|
||||
'.gmail_quote_container',
|
||||
@@ -29,7 +31,7 @@ export class EmailQuoteExtractor {
|
||||
static extractQuotes(htmlContent) {
|
||||
// Create a temporary DOM element to parse HTML
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = htmlContent;
|
||||
tempDiv.innerHTML = DOMPurify.sanitize(htmlContent);
|
||||
|
||||
// Remove elements matching class selectors
|
||||
QUOTE_INDICATORS.forEach(selector => {
|
||||
@@ -56,7 +58,7 @@ export class EmailQuoteExtractor {
|
||||
*/
|
||||
static hasQuotes(htmlContent) {
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = htmlContent;
|
||||
tempDiv.innerHTML = DOMPurify.sanitize(htmlContent);
|
||||
|
||||
// Check for class-based quotes
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
|
||||
@@ -20,6 +20,7 @@ const FEATURE_HELP_URLS = {
|
||||
webhook: 'https://chwt.app/hc/webhooks',
|
||||
billing: 'https://chwt.app/pricing',
|
||||
saml: 'https://chwt.app/hc/saml',
|
||||
captain_billing: 'https://chwt.app/hc/captain_billing',
|
||||
};
|
||||
|
||||
export function getHelpUrlForFeature(featureName) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { format, parseISO, isValid as isValidDate } from 'date-fns';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
/**
|
||||
* Extracts plain text from HTML content
|
||||
@@ -13,7 +14,7 @@ export const extractPlainTextFromHtml = html => {
|
||||
return html.replace(/<[^>]*>/g, ' ');
|
||||
}
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = html;
|
||||
tempDiv.innerHTML = DOMPurify.sanitize(html);
|
||||
return tempDiv.textContent || tempDiv.innerText || '';
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
replaceSignature,
|
||||
cleanSignature,
|
||||
extractTextFromMarkdown,
|
||||
stripUnsupportedSignatureMarkdown,
|
||||
stripUnsupportedMarkdown,
|
||||
insertAtCursor,
|
||||
findNodeToInsertImage,
|
||||
setURLWithQueryAndSize,
|
||||
@@ -145,25 +145,19 @@ describe('appendSignature', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('stripUnsupportedSignatureMarkdown', () => {
|
||||
describe('stripUnsupportedMarkdown', () => {
|
||||
const richSignature =
|
||||
'**Bold** _italic_ [link](http://example.com) ';
|
||||
|
||||
it('keeps all formatting for Email channel (supports image, link, strong, em)', () => {
|
||||
const result = stripUnsupportedSignatureMarkdown(
|
||||
richSignature,
|
||||
'Channel::Email'
|
||||
);
|
||||
const result = stripUnsupportedMarkdown(richSignature, 'Channel::Email');
|
||||
expect(result).toContain('**Bold**');
|
||||
expect(result).toContain('_italic_');
|
||||
expect(result).toContain('[link](http://example.com)');
|
||||
expect(result).toContain('');
|
||||
});
|
||||
it('strips images but keeps bold/italic for Api channel', () => {
|
||||
const result = stripUnsupportedSignatureMarkdown(
|
||||
richSignature,
|
||||
'Channel::Api'
|
||||
);
|
||||
const result = stripUnsupportedMarkdown(richSignature, 'Channel::Api');
|
||||
expect(result).toContain('**Bold**');
|
||||
expect(result).toContain('_italic_');
|
||||
expect(result).toContain('link'); // link text kept
|
||||
@@ -171,20 +165,14 @@ describe('stripUnsupportedSignatureMarkdown', () => {
|
||||
expect(result).not.toContain('; // image removed
|
||||
});
|
||||
it('strips images but keeps bold/italic/link for Telegram channel', () => {
|
||||
const result = stripUnsupportedSignatureMarkdown(
|
||||
richSignature,
|
||||
'Channel::Telegram'
|
||||
);
|
||||
const result = stripUnsupportedMarkdown(richSignature, 'Channel::Telegram');
|
||||
expect(result).toContain('**Bold**');
|
||||
expect(result).toContain('_italic_');
|
||||
expect(result).toContain('[link](http://example.com)');
|
||||
expect(result).not.toContain(';
|
||||
});
|
||||
it('strips all formatting for SMS channel', () => {
|
||||
const result = stripUnsupportedSignatureMarkdown(
|
||||
richSignature,
|
||||
'Channel::Sms'
|
||||
);
|
||||
const result = stripUnsupportedMarkdown(richSignature, 'Channel::Sms');
|
||||
expect(result).toContain('Bold');
|
||||
expect(result).toContain('italic');
|
||||
expect(result).toContain('link');
|
||||
@@ -194,8 +182,52 @@ describe('stripUnsupportedSignatureMarkdown', () => {
|
||||
expect(result).not.toContain(';
|
||||
});
|
||||
it('returns empty string for empty input', () => {
|
||||
expect(stripUnsupportedSignatureMarkdown('', 'Channel::Api')).toBe('');
|
||||
expect(stripUnsupportedSignatureMarkdown(null, 'Channel::Api')).toBe('');
|
||||
expect(stripUnsupportedMarkdown('', 'Channel::Api')).toBe('');
|
||||
expect(stripUnsupportedMarkdown(null, 'Channel::Api')).toBe('');
|
||||
});
|
||||
|
||||
describe('with cleanWhitespace parameter', () => {
|
||||
const textWithWhitespace =
|
||||
'**Bold** text\n\nWith multiple\n\nLine breaks\n\n And spaces ';
|
||||
|
||||
it('cleans whitespace when cleanWhitespace=true (default)', () => {
|
||||
const result = stripUnsupportedMarkdown(
|
||||
textWithWhitespace,
|
||||
'Channel::Api',
|
||||
true
|
||||
);
|
||||
expect(result).toBe(
|
||||
'**Bold** text\nWith multiple\nLine breaks\nAnd spaces'
|
||||
);
|
||||
expect(result).not.toContain('\n\n');
|
||||
expect(result).not.toContain(' ');
|
||||
});
|
||||
|
||||
it('preserves whitespace when cleanWhitespace=false', () => {
|
||||
const result = stripUnsupportedMarkdown(
|
||||
textWithWhitespace,
|
||||
'Channel::Api',
|
||||
false
|
||||
);
|
||||
expect(result).toContain('\n\n');
|
||||
expect(result).toContain(' And spaces ');
|
||||
expect(result).toBe(
|
||||
'**Bold** text\n\nWith multiple\n\nLine breaks\n\n And spaces '
|
||||
);
|
||||
});
|
||||
|
||||
it('strips formatting but preserves whitespace for messages', () => {
|
||||
const messageWithFormatting = '**Bold**\n\n`code`\n\nNormal text';
|
||||
const result = stripUnsupportedMarkdown(
|
||||
messageWithFormatting,
|
||||
'Channel::Sms',
|
||||
false
|
||||
);
|
||||
expect(result).toBe('Bold\n\ncode\n\nNormal text');
|
||||
expect(result).toContain('\n\n');
|
||||
expect(result).not.toContain('**');
|
||||
expect(result).not.toContain('`');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -901,6 +933,17 @@ describe('stripUnsupportedFormatting', () => {
|
||||
expect(stripUnsupportedFormatting(content, fullSchema)).toBe(content);
|
||||
});
|
||||
|
||||
it('preserves various URI scheme autolinks', () => {
|
||||
const content =
|
||||
'Email <mailto:user@example.com> or call <tel:+1234567890>';
|
||||
expect(stripUnsupportedFormatting(content, fullSchema)).toBe(content);
|
||||
});
|
||||
|
||||
it('preserves email autolinks', () => {
|
||||
const content = 'Contact us at <support@chatwoot.com>';
|
||||
expect(stripUnsupportedFormatting(content, fullSchema)).toBe(content);
|
||||
});
|
||||
|
||||
it('preserves lists when schema supports them', () => {
|
||||
const content = '- item 1\n- item 2\n1. first\n2. second';
|
||||
expect(stripUnsupportedFormatting(content, fullSchema)).toBe(content);
|
||||
@@ -984,6 +1027,26 @@ describe('stripUnsupportedFormatting', () => {
|
||||
expect(stripUnsupportedFormatting(content, emptySchema)).toBe(expected);
|
||||
});
|
||||
|
||||
it('converts URI scheme autolinks to plain text', () => {
|
||||
const content =
|
||||
'Email <mailto:support@example.com> or call <tel:+1234567890>';
|
||||
const expected =
|
||||
'Email mailto:support@example.com or call tel:+1234567890';
|
||||
expect(stripUnsupportedFormatting(content, emptySchema)).toBe(expected);
|
||||
});
|
||||
|
||||
it('converts email autolinks to plain text', () => {
|
||||
const content = 'Reach us at <admin@chatwoot.com> for help';
|
||||
const expected = 'Reach us at admin@chatwoot.com for help';
|
||||
expect(stripUnsupportedFormatting(content, emptySchema)).toBe(expected);
|
||||
});
|
||||
|
||||
it('handles mixed autolink types', () => {
|
||||
const content = 'Visit <https://example.com> or email <info@example.com>';
|
||||
const expected = 'Visit https://example.com or email info@example.com';
|
||||
expect(stripUnsupportedFormatting(content, emptySchema)).toBe(expected);
|
||||
});
|
||||
|
||||
it('strips bullet list markers', () => {
|
||||
expect(
|
||||
stripUnsupportedFormatting('- item 1\n- item 2', emptySchema)
|
||||
|
||||
@@ -96,4 +96,58 @@ describe('EmailQuoteExtractor', () => {
|
||||
it('detects quotes for trailing blockquotes even when signatures follow text', () => {
|
||||
expect(EmailQuoteExtractor.hasQuotes(EMAIL_WITH_SIGNATURE)).toBe(true);
|
||||
});
|
||||
|
||||
describe('HTML sanitization', () => {
|
||||
it('removes onerror handlers from img tags in extractQuotes', () => {
|
||||
const maliciousHtml = '<p>Hello</p><img src="x" onerror="alert(1)">';
|
||||
const cleanedHtml = EmailQuoteExtractor.extractQuotes(maliciousHtml);
|
||||
|
||||
expect(cleanedHtml).not.toContain('onerror');
|
||||
expect(cleanedHtml).toContain('<p>Hello</p>');
|
||||
});
|
||||
|
||||
it('removes onerror handlers from img tags in hasQuotes', () => {
|
||||
const maliciousHtml = '<p>Hello</p><img src="x" onerror="alert(1)">';
|
||||
// Should not throw and should safely check for quotes
|
||||
const result = EmailQuoteExtractor.hasQuotes(maliciousHtml);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('removes script tags in extractQuotes', () => {
|
||||
const maliciousHtml =
|
||||
'<p>Content</p><script>alert("xss")</script><p>More</p>';
|
||||
const cleanedHtml = EmailQuoteExtractor.extractQuotes(maliciousHtml);
|
||||
|
||||
expect(cleanedHtml).not.toContain('<script');
|
||||
expect(cleanedHtml).not.toContain('alert');
|
||||
expect(cleanedHtml).toContain('<p>Content</p>');
|
||||
expect(cleanedHtml).toContain('<p>More</p>');
|
||||
});
|
||||
|
||||
it('removes onclick handlers in extractQuotes', () => {
|
||||
const maliciousHtml = '<p onclick="alert(1)">Click me</p>';
|
||||
const cleanedHtml = EmailQuoteExtractor.extractQuotes(maliciousHtml);
|
||||
|
||||
expect(cleanedHtml).not.toContain('onclick');
|
||||
expect(cleanedHtml).toContain('Click me');
|
||||
});
|
||||
|
||||
it('removes javascript: URLs in extractQuotes', () => {
|
||||
const maliciousHtml = '<a href="javascript:alert(1)">Link</a>';
|
||||
const cleanedHtml = EmailQuoteExtractor.extractQuotes(maliciousHtml);
|
||||
|
||||
// eslint-disable-next-line no-script-url
|
||||
expect(cleanedHtml).not.toContain('javascript:');
|
||||
expect(cleanedHtml).toContain('Link');
|
||||
});
|
||||
|
||||
it('removes encoded payloads with event handlers in extractQuotes', () => {
|
||||
const maliciousHtml =
|
||||
'<img src="x" id="PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==" onerror="eval(atob(this.id))">';
|
||||
const cleanedHtml = EmailQuoteExtractor.extractQuotes(maliciousHtml);
|
||||
|
||||
expect(cleanedHtml).not.toContain('onerror');
|
||||
expect(cleanedHtml).not.toContain('eval');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,6 +33,26 @@ describe('quotedEmailHelper', () => {
|
||||
expect(result).toContain('Line 1');
|
||||
expect(result).toContain('Line 2');
|
||||
});
|
||||
|
||||
it('sanitizes onerror handlers from img tags', () => {
|
||||
const html = '<p>Hello</p><img src="x" onerror="alert(1)">';
|
||||
const result = extractPlainTextFromHtml(html);
|
||||
expect(result).toBe('Hello');
|
||||
});
|
||||
|
||||
it('sanitizes script tags', () => {
|
||||
const html = '<p>Safe</p><script>alert(1)</script><p>Content</p>';
|
||||
const result = extractPlainTextFromHtml(html);
|
||||
expect(result).toContain('Safe');
|
||||
expect(result).toContain('Content');
|
||||
expect(result).not.toContain('alert');
|
||||
});
|
||||
|
||||
it('sanitizes onclick handlers', () => {
|
||||
const html = '<p onclick="alert(1)">Click me</p>';
|
||||
const result = extractPlainTextFromHtml(html);
|
||||
expect(result).toBe('Click me');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEmailSenderName', () => {
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"OPTIONS": {
|
||||
"NAME": "Name",
|
||||
"DOMAIN": "Domain",
|
||||
"CREATED_AT": "Created at"
|
||||
"CREATED_AT": "Created at",
|
||||
"CONTACTS_COUNT": "Contacts count"
|
||||
}
|
||||
},
|
||||
"ORDER": {
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"TYPES": {
|
||||
"MEDIA": "Media",
|
||||
"QUICK_REPLY": "Quick Reply",
|
||||
"CALL_TO_ACTION": "Call to Action",
|
||||
"TEXT": "Text"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -275,6 +275,16 @@
|
||||
"SIDEBAR": {
|
||||
"CONTACT": "Contact",
|
||||
"COPILOT": "Copilot"
|
||||
},
|
||||
"VOICE_WIDGET": {
|
||||
"INCOMING_CALL": "Incoming call",
|
||||
"OUTGOING_CALL": "Outgoing call",
|
||||
"CALL_IN_PROGRESS": "Call in progress",
|
||||
"NOT_ANSWERED_YET": "Not answered yet",
|
||||
"HANDLED_IN_ANOTHER_TAB": "Being handled in another tab",
|
||||
"REJECT_CALL": "Reject",
|
||||
"JOIN_CALL": "Join call",
|
||||
"END_CALL": "End call"
|
||||
}
|
||||
},
|
||||
"EMAIL_TRANSCRIPT": {
|
||||
|
||||
@@ -808,6 +808,35 @@
|
||||
"LABEL": "Message",
|
||||
"PLACEHOLDER": "Please enter a message to show users with the form"
|
||||
},
|
||||
"BUTTON_TEXT": {
|
||||
"LABEL": "Button text",
|
||||
"PLACEHOLDER": "Please rate us"
|
||||
},
|
||||
"LANGUAGE": {
|
||||
"LABEL": "Language",
|
||||
"PLACEHOLDER": "Select template language"
|
||||
},
|
||||
"MESSAGE_PREVIEW": {
|
||||
"LABEL": "Message preview",
|
||||
"TOOLTIP": "This may vary slightly when rendered on WhatsApp's platform."
|
||||
},
|
||||
"TEMPLATE_STATUS": {
|
||||
"APPROVED": "Approved by WhatsApp",
|
||||
"PENDING": "Pending WhatsApp approval",
|
||||
"REJECTED": "Meta rejected the template",
|
||||
"DEFAULT": "Needs WhatsApp approval",
|
||||
"NOT_FOUND": "The template does not exist in the Meta platform."
|
||||
},
|
||||
"TEMPLATE_CREATION": {
|
||||
"SUCCESS_MESSAGE": "WhatsApp template created successfully and sent for approval",
|
||||
"ERROR_MESSAGE": "Failed to create WhatsApp template"
|
||||
},
|
||||
"TEMPLATE_UPDATE_DIALOG": {
|
||||
"TITLE": "Edit survey details",
|
||||
"DESCRIPTION": "We will delete the previous template and make a new one which will be sent again for WhatsApp approval",
|
||||
"CONFIRM": "Create new template",
|
||||
"CANCEL": "Go back"
|
||||
},
|
||||
"SURVEY_RULE": {
|
||||
"LABEL": "Survey rule",
|
||||
"DESCRIPTION_PREFIX": "Send the survey if the conversation",
|
||||
@@ -819,6 +848,7 @@
|
||||
"SELECT_PLACEHOLDER": "select labels"
|
||||
},
|
||||
"NOTE": "Note: CSAT surveys are sent only once per conversation",
|
||||
"WHATSAPP_NOTE": "Note: We will create a template and send it for WhatsApp approval. After being approved, surveys will be sent only once per conversation as per the survey rule.",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "CSAT settings updated successfully",
|
||||
"ERROR_MESSAGE": "We couldn't update CSAT settings. Please try again later."
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"SEARCH": {
|
||||
"TABS": {
|
||||
"ALL": "All",
|
||||
"ALL": "All results",
|
||||
"CONTACTS": "Contacts",
|
||||
"CONVERSATIONS": "Conversations",
|
||||
"MESSAGES": "Messages",
|
||||
@@ -19,14 +19,50 @@
|
||||
"LOADING_DATA": "Loading",
|
||||
"EMPTY_STATE": "No {item} found for query '{query}'",
|
||||
"EMPTY_STATE_FULL": "No results found for query '{query}'",
|
||||
"PLACEHOLDER_KEYBINDING": "/ to focus",
|
||||
"PLACEHOLDER_KEYBINDING": "/to focus",
|
||||
"INPUT_PLACEHOLDER": "Type 3 or more characters to search",
|
||||
"RECENT_SEARCHES": "Recent searches",
|
||||
"CLEAR_ALL": "Clear all",
|
||||
"MOST_RECENT": "Most recent",
|
||||
"EMPTY_STATE_DEFAULT": "Search by conversation id, email, phone number, messages for better search results. ",
|
||||
"BOT_LABEL": "Bot",
|
||||
"READ_MORE": "Read more",
|
||||
"READ_LESS": "Read less",
|
||||
"WROTE": "wrote:",
|
||||
"FROM": "from",
|
||||
"EMAIL": "email",
|
||||
"EMAIL_SUBJECT": "subject"
|
||||
"FROM": "From",
|
||||
"EMAIL": "Email",
|
||||
"EMAIL_SUBJECT": "Subject",
|
||||
"PRIVATE": "Private note",
|
||||
"TRANSCRIPT": "Transcript",
|
||||
"CREATED_AT": "created {time}",
|
||||
"UPDATED_AT": "updated {time}",
|
||||
"SORT_BY": {
|
||||
"RELEVANCE": "Relevance"
|
||||
},
|
||||
"DATE_RANGE": {
|
||||
"LAST_7_DAYS": "Last 7 days",
|
||||
"LAST_30_DAYS": "Last 30 days",
|
||||
"LAST_60_DAYS": "Last 60 days",
|
||||
"LAST_90_DAYS": "Last 90 days",
|
||||
"CUSTOM_RANGE": "Custom range:",
|
||||
"CREATED_BETWEEN": "Created between",
|
||||
"AND": "and",
|
||||
"APPLY": "Apply",
|
||||
"BEFORE_DATE": "Before {date}",
|
||||
"AFTER_DATE": "After {date}",
|
||||
"TIME_RANGE": "Filter by time",
|
||||
"CLEAR_FILTER": "Clear filter"
|
||||
},
|
||||
"FILTERS": {
|
||||
"FILTER_MESSAGE": "Filter messages by:",
|
||||
"FROM": "Sender",
|
||||
"IN": "Inbox",
|
||||
"AGENTS": "Agents",
|
||||
"CONTACTS": "Contacts",
|
||||
"INBOXES": "Inboxes",
|
||||
"NO_AGENTS": "No agents found",
|
||||
"NO_CONTACTS": "Start by searching to see results",
|
||||
"NO_INBOXES": "No inboxes found"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"OPTIONS": {
|
||||
"NAME": "الاسم",
|
||||
"DOMAIN": "النطاق",
|
||||
"CREATED_AT": "تم إنشاؤها في"
|
||||
"CREATED_AT": "تم إنشاؤها في",
|
||||
"CONTACTS_COUNT": "Contacts count"
|
||||
}
|
||||
},
|
||||
"ORDER": {
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"TYPES": {
|
||||
"MEDIA": "Media",
|
||||
"QUICK_REPLY": "Quick Reply",
|
||||
"CALL_TO_ACTION": "Call to Action",
|
||||
"TEXT": "النص"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -275,6 +275,16 @@
|
||||
"SIDEBAR": {
|
||||
"CONTACT": "جهات الاتصال",
|
||||
"COPILOT": "Copilot"
|
||||
},
|
||||
"VOICE_WIDGET": {
|
||||
"INCOMING_CALL": "Incoming call",
|
||||
"OUTGOING_CALL": "Outgoing call",
|
||||
"CALL_IN_PROGRESS": "Call in progress",
|
||||
"NOT_ANSWERED_YET": "Not answered yet",
|
||||
"HANDLED_IN_ANOTHER_TAB": "Being handled in another tab",
|
||||
"REJECT_CALL": "Reject",
|
||||
"JOIN_CALL": "Join call",
|
||||
"END_CALL": "End call"
|
||||
}
|
||||
},
|
||||
"EMAIL_TRANSCRIPT": {
|
||||
|
||||
@@ -808,6 +808,35 @@
|
||||
"LABEL": "رسالة",
|
||||
"PLACEHOLDER": "Please enter a message to show users with the form"
|
||||
},
|
||||
"BUTTON_TEXT": {
|
||||
"LABEL": "Button text",
|
||||
"PLACEHOLDER": "Please rate us"
|
||||
},
|
||||
"LANGUAGE": {
|
||||
"LABEL": "اللغة",
|
||||
"PLACEHOLDER": "Select template language"
|
||||
},
|
||||
"MESSAGE_PREVIEW": {
|
||||
"LABEL": "Message preview",
|
||||
"TOOLTIP": "This may vary slightly when rendered on WhatsApp's platform."
|
||||
},
|
||||
"TEMPLATE_STATUS": {
|
||||
"APPROVED": "Approved by WhatsApp",
|
||||
"PENDING": "Pending WhatsApp approval",
|
||||
"REJECTED": "Meta rejected the template",
|
||||
"DEFAULT": "Needs WhatsApp approval",
|
||||
"NOT_FOUND": "The template does not exist in the Meta platform."
|
||||
},
|
||||
"TEMPLATE_CREATION": {
|
||||
"SUCCESS_MESSAGE": "WhatsApp template created successfully and sent for approval",
|
||||
"ERROR_MESSAGE": "Failed to create WhatsApp template"
|
||||
},
|
||||
"TEMPLATE_UPDATE_DIALOG": {
|
||||
"TITLE": "Edit survey details",
|
||||
"DESCRIPTION": "We will delete the previous template and make a new one which will be sent again for WhatsApp approval",
|
||||
"CONFIRM": "Create new template",
|
||||
"CANCEL": "العودة للخلف"
|
||||
},
|
||||
"SURVEY_RULE": {
|
||||
"LABEL": "Survey rule",
|
||||
"DESCRIPTION_PREFIX": "Send the survey if the conversation",
|
||||
@@ -819,6 +848,7 @@
|
||||
"SELECT_PLACEHOLDER": "select labels"
|
||||
},
|
||||
"NOTE": "Note: CSAT surveys are sent only once per conversation",
|
||||
"WHATSAPP_NOTE": "Note: We will create a template and send it for WhatsApp approval. After being approved, surveys will be sent only once per conversation as per the survey rule.",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "CSAT settings updated successfully",
|
||||
"ERROR_MESSAGE": "We couldn't update CSAT settings. Please try again later."
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"SEARCH": {
|
||||
"TABS": {
|
||||
"ALL": "الكل",
|
||||
"ALL": "All results",
|
||||
"CONTACTS": "جهات الاتصال",
|
||||
"CONVERSATIONS": "المحادثات",
|
||||
"MESSAGES": "الرسائل",
|
||||
@@ -19,14 +19,50 @@
|
||||
"LOADING_DATA": "جار التحميل",
|
||||
"EMPTY_STATE": "لم يتم العثور على {item} للطلب '{query}'",
|
||||
"EMPTY_STATE_FULL": "لم يتم العثور على نتائج للطلب '{query}'",
|
||||
"PLACEHOLDER_KEYBINDING": "/ للتركيز",
|
||||
"PLACEHOLDER_KEYBINDING": "/للتركيز",
|
||||
"INPUT_PLACEHOLDER": "أكتب 3 أحرف أو أكثر للبحث",
|
||||
"RECENT_SEARCHES": "Recent searches",
|
||||
"CLEAR_ALL": "Clear all",
|
||||
"MOST_RECENT": "Most recent",
|
||||
"EMPTY_STATE_DEFAULT": "البحث عن طريق معرف المحادثة أو البريد الإلكتروني أو رقم الهاتف أو الرسائل للحصول على نتائج بحث أفضل. ",
|
||||
"BOT_LABEL": "رد آلي",
|
||||
"READ_MORE": "اقرأ المزيد",
|
||||
"READ_LESS": "Read less",
|
||||
"WROTE": "كتب:",
|
||||
"FROM": "من",
|
||||
"EMAIL": "البريد الإلكتروني",
|
||||
"EMAIL_SUBJECT": "الموضوع"
|
||||
"EMAIL_SUBJECT": "الموضوع",
|
||||
"PRIVATE": "Private note",
|
||||
"TRANSCRIPT": "Transcript",
|
||||
"CREATED_AT": "created {time}",
|
||||
"UPDATED_AT": "updated {time}",
|
||||
"SORT_BY": {
|
||||
"RELEVANCE": "Relevance"
|
||||
},
|
||||
"DATE_RANGE": {
|
||||
"LAST_7_DAYS": "آخر 7 أيام",
|
||||
"LAST_30_DAYS": "آخر 30 يوماً",
|
||||
"LAST_60_DAYS": "آخر 60 يوماً",
|
||||
"LAST_90_DAYS": "آخر 90 يوماً",
|
||||
"CUSTOM_RANGE": "Custom range:",
|
||||
"CREATED_BETWEEN": "Created between",
|
||||
"AND": "و",
|
||||
"APPLY": "تطبيق",
|
||||
"BEFORE_DATE": "Before {date}",
|
||||
"AFTER_DATE": "After {date}",
|
||||
"TIME_RANGE": "Filter by time",
|
||||
"CLEAR_FILTER": "Clear filter"
|
||||
},
|
||||
"FILTERS": {
|
||||
"FILTER_MESSAGE": "Filter messages by:",
|
||||
"FROM": "المرسل",
|
||||
"IN": "صندوق الوارد",
|
||||
"AGENTS": "الوكلاء",
|
||||
"CONTACTS": "جهات الاتصال",
|
||||
"INBOXES": "قنوات التواصل",
|
||||
"NO_AGENTS": "لم يتم العثور على وكلاء",
|
||||
"NO_CONTACTS": "Start by searching to see results",
|
||||
"NO_INBOXES": "No inboxes found"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"OPTIONS": {
|
||||
"NAME": "Name",
|
||||
"DOMAIN": "Domain",
|
||||
"CREATED_AT": "Created at"
|
||||
"CREATED_AT": "Created at",
|
||||
"CONTACTS_COUNT": "Contacts count"
|
||||
}
|
||||
},
|
||||
"ORDER": {
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"TYPES": {
|
||||
"MEDIA": "Media",
|
||||
"QUICK_REPLY": "Quick Reply",
|
||||
"CALL_TO_ACTION": "Call to Action",
|
||||
"TEXT": "Text"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -275,6 +275,16 @@
|
||||
"SIDEBAR": {
|
||||
"CONTACT": "Contact",
|
||||
"COPILOT": "Copilot"
|
||||
},
|
||||
"VOICE_WIDGET": {
|
||||
"INCOMING_CALL": "Incoming call",
|
||||
"OUTGOING_CALL": "Outgoing call",
|
||||
"CALL_IN_PROGRESS": "Call in progress",
|
||||
"NOT_ANSWERED_YET": "Not answered yet",
|
||||
"HANDLED_IN_ANOTHER_TAB": "Being handled in another tab",
|
||||
"REJECT_CALL": "Reject",
|
||||
"JOIN_CALL": "Join call",
|
||||
"END_CALL": "End call"
|
||||
}
|
||||
},
|
||||
"EMAIL_TRANSCRIPT": {
|
||||
|
||||
@@ -808,6 +808,35 @@
|
||||
"LABEL": "Message",
|
||||
"PLACEHOLDER": "Please enter a message to show users with the form"
|
||||
},
|
||||
"BUTTON_TEXT": {
|
||||
"LABEL": "Button text",
|
||||
"PLACEHOLDER": "Please rate us"
|
||||
},
|
||||
"LANGUAGE": {
|
||||
"LABEL": "Language",
|
||||
"PLACEHOLDER": "Select template language"
|
||||
},
|
||||
"MESSAGE_PREVIEW": {
|
||||
"LABEL": "Message preview",
|
||||
"TOOLTIP": "This may vary slightly when rendered on WhatsApp's platform."
|
||||
},
|
||||
"TEMPLATE_STATUS": {
|
||||
"APPROVED": "Approved by WhatsApp",
|
||||
"PENDING": "Pending WhatsApp approval",
|
||||
"REJECTED": "Meta rejected the template",
|
||||
"DEFAULT": "Needs WhatsApp approval",
|
||||
"NOT_FOUND": "The template does not exist in the Meta platform."
|
||||
},
|
||||
"TEMPLATE_CREATION": {
|
||||
"SUCCESS_MESSAGE": "WhatsApp template created successfully and sent for approval",
|
||||
"ERROR_MESSAGE": "Failed to create WhatsApp template"
|
||||
},
|
||||
"TEMPLATE_UPDATE_DIALOG": {
|
||||
"TITLE": "Edit survey details",
|
||||
"DESCRIPTION": "We will delete the previous template and make a new one which will be sent again for WhatsApp approval",
|
||||
"CONFIRM": "Create new template",
|
||||
"CANCEL": "Go back"
|
||||
},
|
||||
"SURVEY_RULE": {
|
||||
"LABEL": "Survey rule",
|
||||
"DESCRIPTION_PREFIX": "Send the survey if the conversation",
|
||||
@@ -819,6 +848,7 @@
|
||||
"SELECT_PLACEHOLDER": "select labels"
|
||||
},
|
||||
"NOTE": "Note: CSAT surveys are sent only once per conversation",
|
||||
"WHATSAPP_NOTE": "Note: We will create a template and send it for WhatsApp approval. After being approved, surveys will be sent only once per conversation as per the survey rule.",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "CSAT settings updated successfully",
|
||||
"ERROR_MESSAGE": "We couldn't update CSAT settings. Please try again later."
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"SEARCH": {
|
||||
"TABS": {
|
||||
"ALL": "All",
|
||||
"ALL": "All results",
|
||||
"CONTACTS": "Contacts",
|
||||
"CONVERSATIONS": "Conversations",
|
||||
"MESSAGES": "Messages",
|
||||
@@ -19,14 +19,50 @@
|
||||
"LOADING_DATA": "Loading",
|
||||
"EMPTY_STATE": "No {item} found for query '{query}'",
|
||||
"EMPTY_STATE_FULL": "No results found for query '{query}'",
|
||||
"PLACEHOLDER_KEYBINDING": "/ to focus",
|
||||
"PLACEHOLDER_KEYBINDING": "/to focus",
|
||||
"INPUT_PLACEHOLDER": "Type 3 or more characters to search",
|
||||
"RECENT_SEARCHES": "Recent searches",
|
||||
"CLEAR_ALL": "Clear all",
|
||||
"MOST_RECENT": "Most recent",
|
||||
"EMPTY_STATE_DEFAULT": "Search by conversation id, email, phone number, messages for better search results. ",
|
||||
"BOT_LABEL": "Bot",
|
||||
"READ_MORE": "Read more",
|
||||
"READ_LESS": "Read less",
|
||||
"WROTE": "wrote:",
|
||||
"FROM": "from",
|
||||
"EMAIL": "email",
|
||||
"EMAIL_SUBJECT": "subject"
|
||||
"FROM": "From",
|
||||
"EMAIL": "Email",
|
||||
"EMAIL_SUBJECT": "Subject",
|
||||
"PRIVATE": "Private note",
|
||||
"TRANSCRIPT": "Transcript",
|
||||
"CREATED_AT": "created {time}",
|
||||
"UPDATED_AT": "updated {time}",
|
||||
"SORT_BY": {
|
||||
"RELEVANCE": "Relevance"
|
||||
},
|
||||
"DATE_RANGE": {
|
||||
"LAST_7_DAYS": "Last 7 days",
|
||||
"LAST_30_DAYS": "Last 30 days",
|
||||
"LAST_60_DAYS": "Last 60 days",
|
||||
"LAST_90_DAYS": "Last 90 days",
|
||||
"CUSTOM_RANGE": "Custom range:",
|
||||
"CREATED_BETWEEN": "Created between",
|
||||
"AND": "and",
|
||||
"APPLY": "Apply",
|
||||
"BEFORE_DATE": "Before {date}",
|
||||
"AFTER_DATE": "After {date}",
|
||||
"TIME_RANGE": "Filter by time",
|
||||
"CLEAR_FILTER": "Clear filter"
|
||||
},
|
||||
"FILTERS": {
|
||||
"FILTER_MESSAGE": "Filter messages by:",
|
||||
"FROM": "Sender",
|
||||
"IN": "Inbox",
|
||||
"AGENTS": "Agents",
|
||||
"CONTACTS": "Contacts",
|
||||
"INBOXES": "Inboxes",
|
||||
"NO_AGENTS": "No agents found",
|
||||
"NO_CONTACTS": "Start by searching to see results",
|
||||
"NO_INBOXES": "No inboxes found"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"OPTIONS": {
|
||||
"NAME": "Име",
|
||||
"DOMAIN": "Domain",
|
||||
"CREATED_AT": "Създаден в"
|
||||
"CREATED_AT": "Създаден в",
|
||||
"CONTACTS_COUNT": "Contacts count"
|
||||
}
|
||||
},
|
||||
"ORDER": {
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"TYPES": {
|
||||
"MEDIA": "Media",
|
||||
"QUICK_REPLY": "Quick Reply",
|
||||
"CALL_TO_ACTION": "Call to Action",
|
||||
"TEXT": "Text"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -275,6 +275,16 @@
|
||||
"SIDEBAR": {
|
||||
"CONTACT": "Контакт",
|
||||
"COPILOT": "Copilot"
|
||||
},
|
||||
"VOICE_WIDGET": {
|
||||
"INCOMING_CALL": "Incoming call",
|
||||
"OUTGOING_CALL": "Outgoing call",
|
||||
"CALL_IN_PROGRESS": "Call in progress",
|
||||
"NOT_ANSWERED_YET": "Not answered yet",
|
||||
"HANDLED_IN_ANOTHER_TAB": "Being handled in another tab",
|
||||
"REJECT_CALL": "Reject",
|
||||
"JOIN_CALL": "Join call",
|
||||
"END_CALL": "End call"
|
||||
}
|
||||
},
|
||||
"EMAIL_TRANSCRIPT": {
|
||||
|
||||
@@ -808,6 +808,35 @@
|
||||
"LABEL": "Съобщение",
|
||||
"PLACEHOLDER": "Please enter a message to show users with the form"
|
||||
},
|
||||
"BUTTON_TEXT": {
|
||||
"LABEL": "Button text",
|
||||
"PLACEHOLDER": "Please rate us"
|
||||
},
|
||||
"LANGUAGE": {
|
||||
"LABEL": "Language",
|
||||
"PLACEHOLDER": "Select template language"
|
||||
},
|
||||
"MESSAGE_PREVIEW": {
|
||||
"LABEL": "Message preview",
|
||||
"TOOLTIP": "This may vary slightly when rendered on WhatsApp's platform."
|
||||
},
|
||||
"TEMPLATE_STATUS": {
|
||||
"APPROVED": "Approved by WhatsApp",
|
||||
"PENDING": "Pending WhatsApp approval",
|
||||
"REJECTED": "Meta rejected the template",
|
||||
"DEFAULT": "Needs WhatsApp approval",
|
||||
"NOT_FOUND": "The template does not exist in the Meta platform."
|
||||
},
|
||||
"TEMPLATE_CREATION": {
|
||||
"SUCCESS_MESSAGE": "WhatsApp template created successfully and sent for approval",
|
||||
"ERROR_MESSAGE": "Failed to create WhatsApp template"
|
||||
},
|
||||
"TEMPLATE_UPDATE_DIALOG": {
|
||||
"TITLE": "Edit survey details",
|
||||
"DESCRIPTION": "We will delete the previous template and make a new one which will be sent again for WhatsApp approval",
|
||||
"CONFIRM": "Create new template",
|
||||
"CANCEL": "Go back"
|
||||
},
|
||||
"SURVEY_RULE": {
|
||||
"LABEL": "Survey rule",
|
||||
"DESCRIPTION_PREFIX": "Send the survey if the conversation",
|
||||
@@ -819,6 +848,7 @@
|
||||
"SELECT_PLACEHOLDER": "select labels"
|
||||
},
|
||||
"NOTE": "Note: CSAT surveys are sent only once per conversation",
|
||||
"WHATSAPP_NOTE": "Note: We will create a template and send it for WhatsApp approval. After being approved, surveys will be sent only once per conversation as per the survey rule.",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "CSAT settings updated successfully",
|
||||
"ERROR_MESSAGE": "We couldn't update CSAT settings. Please try again later."
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"SEARCH": {
|
||||
"TABS": {
|
||||
"ALL": "Всички",
|
||||
"ALL": "All results",
|
||||
"CONTACTS": "Контакти",
|
||||
"CONVERSATIONS": "Разговори",
|
||||
"MESSAGES": "Messages",
|
||||
@@ -19,14 +19,50 @@
|
||||
"LOADING_DATA": "Loading",
|
||||
"EMPTY_STATE": "No {item} found for query '{query}'",
|
||||
"EMPTY_STATE_FULL": "No results found for query '{query}'",
|
||||
"PLACEHOLDER_KEYBINDING": "/ to focus",
|
||||
"PLACEHOLDER_KEYBINDING": "/to focus",
|
||||
"INPUT_PLACEHOLDER": "Search messages, contacts or conversations",
|
||||
"RECENT_SEARCHES": "Recent searches",
|
||||
"CLEAR_ALL": "Clear all",
|
||||
"MOST_RECENT": "Most recent",
|
||||
"EMPTY_STATE_DEFAULT": "Search by conversation id, email, phone number, messages for better search results.",
|
||||
"BOT_LABEL": "Бот",
|
||||
"READ_MORE": "Read more",
|
||||
"READ_LESS": "Read less",
|
||||
"WROTE": "wrote:",
|
||||
"FROM": "от",
|
||||
"EMAIL": "имейл",
|
||||
"EMAIL_SUBJECT": "тема"
|
||||
"FROM": "From",
|
||||
"EMAIL": "Email",
|
||||
"EMAIL_SUBJECT": "Subject",
|
||||
"PRIVATE": "Private note",
|
||||
"TRANSCRIPT": "Transcript",
|
||||
"CREATED_AT": "created {time}",
|
||||
"UPDATED_AT": "updated {time}",
|
||||
"SORT_BY": {
|
||||
"RELEVANCE": "Relevance"
|
||||
},
|
||||
"DATE_RANGE": {
|
||||
"LAST_7_DAYS": "Last 7 days",
|
||||
"LAST_30_DAYS": "Last 30 days",
|
||||
"LAST_60_DAYS": "Last 60 days",
|
||||
"LAST_90_DAYS": "Last 90 days",
|
||||
"CUSTOM_RANGE": "Custom range:",
|
||||
"CREATED_BETWEEN": "Created between",
|
||||
"AND": "and",
|
||||
"APPLY": "Apply",
|
||||
"BEFORE_DATE": "Before {date}",
|
||||
"AFTER_DATE": "After {date}",
|
||||
"TIME_RANGE": "Filter by time",
|
||||
"CLEAR_FILTER": "Clear filter"
|
||||
},
|
||||
"FILTERS": {
|
||||
"FILTER_MESSAGE": "Filter messages by:",
|
||||
"FROM": "Sender",
|
||||
"IN": "Входяща кутия",
|
||||
"AGENTS": "Агенти",
|
||||
"CONTACTS": "Контакти",
|
||||
"INBOXES": "Inboxes",
|
||||
"NO_AGENTS": "Няма намерени агенти",
|
||||
"NO_CONTACTS": "Start by searching to see results",
|
||||
"NO_INBOXES": "No inboxes found"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"OPTIONS": {
|
||||
"NAME": "Name",
|
||||
"DOMAIN": "Domain",
|
||||
"CREATED_AT": "Created at"
|
||||
"CREATED_AT": "Created at",
|
||||
"CONTACTS_COUNT": "Contacts count"
|
||||
}
|
||||
},
|
||||
"ORDER": {
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"TYPES": {
|
||||
"MEDIA": "Media",
|
||||
"QUICK_REPLY": "Quick Reply",
|
||||
"CALL_TO_ACTION": "Call to Action",
|
||||
"TEXT": "Text"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -275,6 +275,16 @@
|
||||
"SIDEBAR": {
|
||||
"CONTACT": "Contact",
|
||||
"COPILOT": "Copilot"
|
||||
},
|
||||
"VOICE_WIDGET": {
|
||||
"INCOMING_CALL": "Incoming call",
|
||||
"OUTGOING_CALL": "Outgoing call",
|
||||
"CALL_IN_PROGRESS": "Call in progress",
|
||||
"NOT_ANSWERED_YET": "Not answered yet",
|
||||
"HANDLED_IN_ANOTHER_TAB": "Being handled in another tab",
|
||||
"REJECT_CALL": "Reject",
|
||||
"JOIN_CALL": "Join call",
|
||||
"END_CALL": "End call"
|
||||
}
|
||||
},
|
||||
"EMAIL_TRANSCRIPT": {
|
||||
|
||||
@@ -808,6 +808,35 @@
|
||||
"LABEL": "Message",
|
||||
"PLACEHOLDER": "Please enter a message to show users with the form"
|
||||
},
|
||||
"BUTTON_TEXT": {
|
||||
"LABEL": "Button text",
|
||||
"PLACEHOLDER": "Please rate us"
|
||||
},
|
||||
"LANGUAGE": {
|
||||
"LABEL": "Language",
|
||||
"PLACEHOLDER": "Select template language"
|
||||
},
|
||||
"MESSAGE_PREVIEW": {
|
||||
"LABEL": "Message preview",
|
||||
"TOOLTIP": "This may vary slightly when rendered on WhatsApp's platform."
|
||||
},
|
||||
"TEMPLATE_STATUS": {
|
||||
"APPROVED": "Approved by WhatsApp",
|
||||
"PENDING": "Pending WhatsApp approval",
|
||||
"REJECTED": "Meta rejected the template",
|
||||
"DEFAULT": "Needs WhatsApp approval",
|
||||
"NOT_FOUND": "The template does not exist in the Meta platform."
|
||||
},
|
||||
"TEMPLATE_CREATION": {
|
||||
"SUCCESS_MESSAGE": "WhatsApp template created successfully and sent for approval",
|
||||
"ERROR_MESSAGE": "Failed to create WhatsApp template"
|
||||
},
|
||||
"TEMPLATE_UPDATE_DIALOG": {
|
||||
"TITLE": "Edit survey details",
|
||||
"DESCRIPTION": "We will delete the previous template and make a new one which will be sent again for WhatsApp approval",
|
||||
"CONFIRM": "Create new template",
|
||||
"CANCEL": "Go back"
|
||||
},
|
||||
"SURVEY_RULE": {
|
||||
"LABEL": "Survey rule",
|
||||
"DESCRIPTION_PREFIX": "Send the survey if the conversation",
|
||||
@@ -819,6 +848,7 @@
|
||||
"SELECT_PLACEHOLDER": "select labels"
|
||||
},
|
||||
"NOTE": "Note: CSAT surveys are sent only once per conversation",
|
||||
"WHATSAPP_NOTE": "Note: We will create a template and send it for WhatsApp approval. After being approved, surveys will be sent only once per conversation as per the survey rule.",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "CSAT settings updated successfully",
|
||||
"ERROR_MESSAGE": "We couldn't update CSAT settings. Please try again later."
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"SEARCH": {
|
||||
"TABS": {
|
||||
"ALL": "All",
|
||||
"ALL": "All results",
|
||||
"CONTACTS": "Contacts",
|
||||
"CONVERSATIONS": "Conversations",
|
||||
"MESSAGES": "Messages",
|
||||
@@ -19,14 +19,50 @@
|
||||
"LOADING_DATA": "Loading",
|
||||
"EMPTY_STATE": "No {item} found for query '{query}'",
|
||||
"EMPTY_STATE_FULL": "No results found for query '{query}'",
|
||||
"PLACEHOLDER_KEYBINDING": "/ to focus",
|
||||
"PLACEHOLDER_KEYBINDING": "/to focus",
|
||||
"INPUT_PLACEHOLDER": "Type 3 or more characters to search",
|
||||
"RECENT_SEARCHES": "Recent searches",
|
||||
"CLEAR_ALL": "Clear all",
|
||||
"MOST_RECENT": "Most recent",
|
||||
"EMPTY_STATE_DEFAULT": "Search by conversation id, email, phone number, messages for better search results. ",
|
||||
"BOT_LABEL": "Bot",
|
||||
"READ_MORE": "Read more",
|
||||
"READ_LESS": "Read less",
|
||||
"WROTE": "wrote:",
|
||||
"FROM": "from",
|
||||
"EMAIL": "email",
|
||||
"EMAIL_SUBJECT": "subject"
|
||||
"FROM": "From",
|
||||
"EMAIL": "Email",
|
||||
"EMAIL_SUBJECT": "Subject",
|
||||
"PRIVATE": "Private note",
|
||||
"TRANSCRIPT": "Transcript",
|
||||
"CREATED_AT": "created {time}",
|
||||
"UPDATED_AT": "updated {time}",
|
||||
"SORT_BY": {
|
||||
"RELEVANCE": "Relevance"
|
||||
},
|
||||
"DATE_RANGE": {
|
||||
"LAST_7_DAYS": "Last 7 days",
|
||||
"LAST_30_DAYS": "Last 30 days",
|
||||
"LAST_60_DAYS": "Last 60 days",
|
||||
"LAST_90_DAYS": "Last 90 days",
|
||||
"CUSTOM_RANGE": "Custom range:",
|
||||
"CREATED_BETWEEN": "Created between",
|
||||
"AND": "and",
|
||||
"APPLY": "Apply",
|
||||
"BEFORE_DATE": "Before {date}",
|
||||
"AFTER_DATE": "After {date}",
|
||||
"TIME_RANGE": "Filter by time",
|
||||
"CLEAR_FILTER": "Clear filter"
|
||||
},
|
||||
"FILTERS": {
|
||||
"FILTER_MESSAGE": "Filter messages by:",
|
||||
"FROM": "Sender",
|
||||
"IN": "Inbox",
|
||||
"AGENTS": "Agents",
|
||||
"CONTACTS": "Contacts",
|
||||
"INBOXES": "Inboxes",
|
||||
"NO_AGENTS": "No agents found",
|
||||
"NO_CONTACTS": "Start by searching to see results",
|
||||
"NO_INBOXES": "No inboxes found"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"OPTIONS": {
|
||||
"NAME": "Nom",
|
||||
"DOMAIN": "Domini",
|
||||
"CREATED_AT": "Creat per"
|
||||
"CREATED_AT": "Creat per",
|
||||
"CONTACTS_COUNT": "Contacts count"
|
||||
}
|
||||
},
|
||||
"ORDER": {
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"TYPES": {
|
||||
"MEDIA": "Media",
|
||||
"QUICK_REPLY": "Quick Reply",
|
||||
"CALL_TO_ACTION": "Call to Action",
|
||||
"TEXT": "Llista"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -275,6 +275,16 @@
|
||||
"SIDEBAR": {
|
||||
"CONTACT": "Contacte",
|
||||
"COPILOT": "Copilot"
|
||||
},
|
||||
"VOICE_WIDGET": {
|
||||
"INCOMING_CALL": "Incoming call",
|
||||
"OUTGOING_CALL": "Outgoing call",
|
||||
"CALL_IN_PROGRESS": "Call in progress",
|
||||
"NOT_ANSWERED_YET": "Not answered yet",
|
||||
"HANDLED_IN_ANOTHER_TAB": "Being handled in another tab",
|
||||
"REJECT_CALL": "Reject",
|
||||
"JOIN_CALL": "Join call",
|
||||
"END_CALL": "End call"
|
||||
}
|
||||
},
|
||||
"EMAIL_TRANSCRIPT": {
|
||||
|
||||
@@ -808,6 +808,35 @@
|
||||
"LABEL": "Missatge",
|
||||
"PLACEHOLDER": "Please enter a message to show users with the form"
|
||||
},
|
||||
"BUTTON_TEXT": {
|
||||
"LABEL": "Button text",
|
||||
"PLACEHOLDER": "Please rate us"
|
||||
},
|
||||
"LANGUAGE": {
|
||||
"LABEL": "Idioma",
|
||||
"PLACEHOLDER": "Select template language"
|
||||
},
|
||||
"MESSAGE_PREVIEW": {
|
||||
"LABEL": "Message preview",
|
||||
"TOOLTIP": "This may vary slightly when rendered on WhatsApp's platform."
|
||||
},
|
||||
"TEMPLATE_STATUS": {
|
||||
"APPROVED": "Approved by WhatsApp",
|
||||
"PENDING": "Pending WhatsApp approval",
|
||||
"REJECTED": "Meta rejected the template",
|
||||
"DEFAULT": "Needs WhatsApp approval",
|
||||
"NOT_FOUND": "The template does not exist in the Meta platform."
|
||||
},
|
||||
"TEMPLATE_CREATION": {
|
||||
"SUCCESS_MESSAGE": "WhatsApp template created successfully and sent for approval",
|
||||
"ERROR_MESSAGE": "Failed to create WhatsApp template"
|
||||
},
|
||||
"TEMPLATE_UPDATE_DIALOG": {
|
||||
"TITLE": "Edit survey details",
|
||||
"DESCRIPTION": "We will delete the previous template and make a new one which will be sent again for WhatsApp approval",
|
||||
"CONFIRM": "Create new template",
|
||||
"CANCEL": "Torna"
|
||||
},
|
||||
"SURVEY_RULE": {
|
||||
"LABEL": "Survey rule",
|
||||
"DESCRIPTION_PREFIX": "Send the survey if the conversation",
|
||||
@@ -819,6 +848,7 @@
|
||||
"SELECT_PLACEHOLDER": "select labels"
|
||||
},
|
||||
"NOTE": "Note: CSAT surveys are sent only once per conversation",
|
||||
"WHATSAPP_NOTE": "Note: We will create a template and send it for WhatsApp approval. After being approved, surveys will be sent only once per conversation as per the survey rule.",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "CSAT settings updated successfully",
|
||||
"ERROR_MESSAGE": "We couldn't update CSAT settings. Please try again later."
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"SEARCH": {
|
||||
"TABS": {
|
||||
"ALL": "Totes",
|
||||
"ALL": "All results",
|
||||
"CONTACTS": "Contactes",
|
||||
"CONVERSATIONS": "Converses",
|
||||
"MESSAGES": "Missatges",
|
||||
@@ -19,14 +19,50 @@
|
||||
"LOADING_DATA": "Carregant",
|
||||
"EMPTY_STATE": "No s'ha trobat cap {item} per a la consulta '{query}'",
|
||||
"EMPTY_STATE_FULL": "No s'han trobat resultats per a la consulta '{query}'",
|
||||
"PLACEHOLDER_KEYBINDING": "/ centrar",
|
||||
"PLACEHOLDER_KEYBINDING": "/centrar",
|
||||
"INPUT_PLACEHOLDER": "Search messages, contacts or conversations",
|
||||
"RECENT_SEARCHES": "Recent searches",
|
||||
"CLEAR_ALL": "Esborrar tot",
|
||||
"MOST_RECENT": "Most recent",
|
||||
"EMPTY_STATE_DEFAULT": "Search by conversation id, email, phone number, messages for better search results.",
|
||||
"BOT_LABEL": "Bot",
|
||||
"READ_MORE": "Llegir més",
|
||||
"READ_LESS": "Read less",
|
||||
"WROTE": "va escriure:",
|
||||
"FROM": "des de",
|
||||
"EMAIL": "correu electrònic",
|
||||
"EMAIL_SUBJECT": "assumpte"
|
||||
"FROM": "Des de",
|
||||
"EMAIL": "Correu electrònic",
|
||||
"EMAIL_SUBJECT": "Assumpte",
|
||||
"PRIVATE": "Private note",
|
||||
"TRANSCRIPT": "Transcript",
|
||||
"CREATED_AT": "created {time}",
|
||||
"UPDATED_AT": "updated {time}",
|
||||
"SORT_BY": {
|
||||
"RELEVANCE": "Relevance"
|
||||
},
|
||||
"DATE_RANGE": {
|
||||
"LAST_7_DAYS": "Últims 7 dies",
|
||||
"LAST_30_DAYS": "Últims 30 dies",
|
||||
"LAST_60_DAYS": "Últims 60 dies",
|
||||
"LAST_90_DAYS": "Últims 90 dies",
|
||||
"CUSTOM_RANGE": "Custom range:",
|
||||
"CREATED_BETWEEN": "Created between",
|
||||
"AND": "and",
|
||||
"APPLY": "Aplica",
|
||||
"BEFORE_DATE": "Before {date}",
|
||||
"AFTER_DATE": "After {date}",
|
||||
"TIME_RANGE": "Filter by time",
|
||||
"CLEAR_FILTER": "Esborra els filtres"
|
||||
},
|
||||
"FILTERS": {
|
||||
"FILTER_MESSAGE": "Filter messages by:",
|
||||
"FROM": "Remitent",
|
||||
"IN": "Safata d'entrada",
|
||||
"AGENTS": "Agents",
|
||||
"CONTACTS": "Contactes",
|
||||
"INBOXES": "Safates d'entrada",
|
||||
"NO_AGENTS": "No s'han trobat agents",
|
||||
"NO_CONTACTS": "Start by searching to see results",
|
||||
"NO_INBOXES": "No inboxes found"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"OPTIONS": {
|
||||
"NAME": "Název",
|
||||
"DOMAIN": "Domain",
|
||||
"CREATED_AT": "Vytvořeno"
|
||||
"CREATED_AT": "Vytvořeno",
|
||||
"CONTACTS_COUNT": "Contacts count"
|
||||
}
|
||||
},
|
||||
"ORDER": {
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"TYPES": {
|
||||
"MEDIA": "Media",
|
||||
"QUICK_REPLY": "Quick Reply",
|
||||
"CALL_TO_ACTION": "Call to Action",
|
||||
"TEXT": "Text"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -275,6 +275,16 @@
|
||||
"SIDEBAR": {
|
||||
"CONTACT": "Contact",
|
||||
"COPILOT": "Copilot"
|
||||
},
|
||||
"VOICE_WIDGET": {
|
||||
"INCOMING_CALL": "Incoming call",
|
||||
"OUTGOING_CALL": "Outgoing call",
|
||||
"CALL_IN_PROGRESS": "Call in progress",
|
||||
"NOT_ANSWERED_YET": "Not answered yet",
|
||||
"HANDLED_IN_ANOTHER_TAB": "Being handled in another tab",
|
||||
"REJECT_CALL": "Reject",
|
||||
"JOIN_CALL": "Join call",
|
||||
"END_CALL": "End call"
|
||||
}
|
||||
},
|
||||
"EMAIL_TRANSCRIPT": {
|
||||
|
||||
@@ -808,6 +808,35 @@
|
||||
"LABEL": "Zpráva",
|
||||
"PLACEHOLDER": "Please enter a message to show users with the form"
|
||||
},
|
||||
"BUTTON_TEXT": {
|
||||
"LABEL": "Button text",
|
||||
"PLACEHOLDER": "Please rate us"
|
||||
},
|
||||
"LANGUAGE": {
|
||||
"LABEL": "Language",
|
||||
"PLACEHOLDER": "Select template language"
|
||||
},
|
||||
"MESSAGE_PREVIEW": {
|
||||
"LABEL": "Message preview",
|
||||
"TOOLTIP": "This may vary slightly when rendered on WhatsApp's platform."
|
||||
},
|
||||
"TEMPLATE_STATUS": {
|
||||
"APPROVED": "Approved by WhatsApp",
|
||||
"PENDING": "Pending WhatsApp approval",
|
||||
"REJECTED": "Meta rejected the template",
|
||||
"DEFAULT": "Needs WhatsApp approval",
|
||||
"NOT_FOUND": "The template does not exist in the Meta platform."
|
||||
},
|
||||
"TEMPLATE_CREATION": {
|
||||
"SUCCESS_MESSAGE": "WhatsApp template created successfully and sent for approval",
|
||||
"ERROR_MESSAGE": "Failed to create WhatsApp template"
|
||||
},
|
||||
"TEMPLATE_UPDATE_DIALOG": {
|
||||
"TITLE": "Edit survey details",
|
||||
"DESCRIPTION": "We will delete the previous template and make a new one which will be sent again for WhatsApp approval",
|
||||
"CONFIRM": "Create new template",
|
||||
"CANCEL": "Go back"
|
||||
},
|
||||
"SURVEY_RULE": {
|
||||
"LABEL": "Survey rule",
|
||||
"DESCRIPTION_PREFIX": "Send the survey if the conversation",
|
||||
@@ -819,6 +848,7 @@
|
||||
"SELECT_PLACEHOLDER": "select labels"
|
||||
},
|
||||
"NOTE": "Note: CSAT surveys are sent only once per conversation",
|
||||
"WHATSAPP_NOTE": "Note: We will create a template and send it for WhatsApp approval. After being approved, surveys will be sent only once per conversation as per the survey rule.",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "CSAT settings updated successfully",
|
||||
"ERROR_MESSAGE": "We couldn't update CSAT settings. Please try again later."
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"SEARCH": {
|
||||
"TABS": {
|
||||
"ALL": "Vše",
|
||||
"ALL": "All results",
|
||||
"CONTACTS": "Kontakty",
|
||||
"CONVERSATIONS": "Konverzace",
|
||||
"MESSAGES": "Zprávy",
|
||||
@@ -19,14 +19,50 @@
|
||||
"LOADING_DATA": "Loading",
|
||||
"EMPTY_STATE": "No {item} found for query '{query}'",
|
||||
"EMPTY_STATE_FULL": "No results found for query '{query}'",
|
||||
"PLACEHOLDER_KEYBINDING": "/ to focus",
|
||||
"PLACEHOLDER_KEYBINDING": "/to focus",
|
||||
"INPUT_PLACEHOLDER": "Search messages, contacts or conversations",
|
||||
"RECENT_SEARCHES": "Recent searches",
|
||||
"CLEAR_ALL": "Clear all",
|
||||
"MOST_RECENT": "Most recent",
|
||||
"EMPTY_STATE_DEFAULT": "Search by conversation id, email, phone number, messages for better search results.",
|
||||
"BOT_LABEL": "Bot",
|
||||
"READ_MORE": "Read more",
|
||||
"READ_LESS": "Read less",
|
||||
"WROTE": "wrote:",
|
||||
"FROM": "od",
|
||||
"EMAIL": "e-mailová adresa",
|
||||
"EMAIL_SUBJECT": "předmět"
|
||||
"FROM": "Od",
|
||||
"EMAIL": "E-mailová adresa",
|
||||
"EMAIL_SUBJECT": "Předmět",
|
||||
"PRIVATE": "Private note",
|
||||
"TRANSCRIPT": "Transcript",
|
||||
"CREATED_AT": "created {time}",
|
||||
"UPDATED_AT": "updated {time}",
|
||||
"SORT_BY": {
|
||||
"RELEVANCE": "Relevance"
|
||||
},
|
||||
"DATE_RANGE": {
|
||||
"LAST_7_DAYS": "Posledních 7 dní",
|
||||
"LAST_30_DAYS": "Posledních 30 dní",
|
||||
"LAST_60_DAYS": "Posledních 60 dní",
|
||||
"LAST_90_DAYS": "Posledních 90 dní",
|
||||
"CUSTOM_RANGE": "Custom range:",
|
||||
"CREATED_BETWEEN": "Created between",
|
||||
"AND": "and",
|
||||
"APPLY": "Použít",
|
||||
"BEFORE_DATE": "Before {date}",
|
||||
"AFTER_DATE": "After {date}",
|
||||
"TIME_RANGE": "Filter by time",
|
||||
"CLEAR_FILTER": "Clear filter"
|
||||
},
|
||||
"FILTERS": {
|
||||
"FILTER_MESSAGE": "Filter messages by:",
|
||||
"FROM": "Sender",
|
||||
"IN": "Inbox",
|
||||
"AGENTS": "Agenti",
|
||||
"CONTACTS": "Kontakty",
|
||||
"INBOXES": "Schránky",
|
||||
"NO_AGENTS": "Nenalezeni žádní agenti",
|
||||
"NO_CONTACTS": "Start by searching to see results",
|
||||
"NO_INBOXES": "No inboxes found"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"OPTIONS": {
|
||||
"NAME": "Navn",
|
||||
"DOMAIN": "Domæne",
|
||||
"CREATED_AT": "Oprettet den"
|
||||
"CREATED_AT": "Oprettet den",
|
||||
"CONTACTS_COUNT": "Contacts count"
|
||||
}
|
||||
},
|
||||
"ORDER": {
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"TYPES": {
|
||||
"MEDIA": "Media",
|
||||
"QUICK_REPLY": "Quick Reply",
|
||||
"CALL_TO_ACTION": "Call to Action",
|
||||
"TEXT": "Tekst"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -275,6 +275,16 @@
|
||||
"SIDEBAR": {
|
||||
"CONTACT": "Kontakt",
|
||||
"COPILOT": "Copilot"
|
||||
},
|
||||
"VOICE_WIDGET": {
|
||||
"INCOMING_CALL": "Incoming call",
|
||||
"OUTGOING_CALL": "Outgoing call",
|
||||
"CALL_IN_PROGRESS": "Call in progress",
|
||||
"NOT_ANSWERED_YET": "Not answered yet",
|
||||
"HANDLED_IN_ANOTHER_TAB": "Being handled in another tab",
|
||||
"REJECT_CALL": "Reject",
|
||||
"JOIN_CALL": "Join call",
|
||||
"END_CALL": "End call"
|
||||
}
|
||||
},
|
||||
"EMAIL_TRANSCRIPT": {
|
||||
|
||||
@@ -808,6 +808,35 @@
|
||||
"LABEL": "Besked",
|
||||
"PLACEHOLDER": "Please enter a message to show users with the form"
|
||||
},
|
||||
"BUTTON_TEXT": {
|
||||
"LABEL": "Button text",
|
||||
"PLACEHOLDER": "Please rate us"
|
||||
},
|
||||
"LANGUAGE": {
|
||||
"LABEL": "Sprog",
|
||||
"PLACEHOLDER": "Select template language"
|
||||
},
|
||||
"MESSAGE_PREVIEW": {
|
||||
"LABEL": "Message preview",
|
||||
"TOOLTIP": "This may vary slightly when rendered on WhatsApp's platform."
|
||||
},
|
||||
"TEMPLATE_STATUS": {
|
||||
"APPROVED": "Approved by WhatsApp",
|
||||
"PENDING": "Pending WhatsApp approval",
|
||||
"REJECTED": "Meta rejected the template",
|
||||
"DEFAULT": "Needs WhatsApp approval",
|
||||
"NOT_FOUND": "The template does not exist in the Meta platform."
|
||||
},
|
||||
"TEMPLATE_CREATION": {
|
||||
"SUCCESS_MESSAGE": "WhatsApp template created successfully and sent for approval",
|
||||
"ERROR_MESSAGE": "Failed to create WhatsApp template"
|
||||
},
|
||||
"TEMPLATE_UPDATE_DIALOG": {
|
||||
"TITLE": "Edit survey details",
|
||||
"DESCRIPTION": "We will delete the previous template and make a new one which will be sent again for WhatsApp approval",
|
||||
"CONFIRM": "Create new template",
|
||||
"CANCEL": "Gå tilbage"
|
||||
},
|
||||
"SURVEY_RULE": {
|
||||
"LABEL": "Survey rule",
|
||||
"DESCRIPTION_PREFIX": "Send the survey if the conversation",
|
||||
@@ -819,6 +848,7 @@
|
||||
"SELECT_PLACEHOLDER": "select labels"
|
||||
},
|
||||
"NOTE": "Note: CSAT surveys are sent only once per conversation",
|
||||
"WHATSAPP_NOTE": "Note: We will create a template and send it for WhatsApp approval. After being approved, surveys will be sent only once per conversation as per the survey rule.",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "CSAT settings updated successfully",
|
||||
"ERROR_MESSAGE": "We couldn't update CSAT settings. Please try again later."
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"SEARCH": {
|
||||
"TABS": {
|
||||
"ALL": "Alle",
|
||||
"ALL": "All results",
|
||||
"CONTACTS": "Kontakter",
|
||||
"CONVERSATIONS": "Samtaler",
|
||||
"MESSAGES": "Beskeder",
|
||||
@@ -19,14 +19,50 @@
|
||||
"LOADING_DATA": "Loading",
|
||||
"EMPTY_STATE": "No {item} found for query '{query}'",
|
||||
"EMPTY_STATE_FULL": "No results found for query '{query}'",
|
||||
"PLACEHOLDER_KEYBINDING": "/ to focus",
|
||||
"PLACEHOLDER_KEYBINDING": "/to focus",
|
||||
"INPUT_PLACEHOLDER": "Search messages, contacts or conversations",
|
||||
"RECENT_SEARCHES": "Recent searches",
|
||||
"CLEAR_ALL": "Clear all",
|
||||
"MOST_RECENT": "Most recent",
|
||||
"EMPTY_STATE_DEFAULT": "Search by conversation id, email, phone number, messages for better search results.",
|
||||
"BOT_LABEL": "Bot",
|
||||
"READ_MORE": "Read more",
|
||||
"READ_LESS": "Read less",
|
||||
"WROTE": "wrote:",
|
||||
"FROM": "fra",
|
||||
"EMAIL": "e-mail",
|
||||
"EMAIL_SUBJECT": "emne"
|
||||
"FROM": "Fra",
|
||||
"EMAIL": "E-mail",
|
||||
"EMAIL_SUBJECT": "Emne",
|
||||
"PRIVATE": "Private note",
|
||||
"TRANSCRIPT": "Transcript",
|
||||
"CREATED_AT": "created {time}",
|
||||
"UPDATED_AT": "updated {time}",
|
||||
"SORT_BY": {
|
||||
"RELEVANCE": "Relevance"
|
||||
},
|
||||
"DATE_RANGE": {
|
||||
"LAST_7_DAYS": "Seneste 7 dage",
|
||||
"LAST_30_DAYS": "Seneste 30 dage",
|
||||
"LAST_60_DAYS": "Seneste 60 dage",
|
||||
"LAST_90_DAYS": "Seneste 90 dage",
|
||||
"CUSTOM_RANGE": "Custom range:",
|
||||
"CREATED_BETWEEN": "Created between",
|
||||
"AND": "and",
|
||||
"APPLY": "Anvend",
|
||||
"BEFORE_DATE": "Before {date}",
|
||||
"AFTER_DATE": "After {date}",
|
||||
"TIME_RANGE": "Filter by time",
|
||||
"CLEAR_FILTER": "Clear filter"
|
||||
},
|
||||
"FILTERS": {
|
||||
"FILTER_MESSAGE": "Filter messages by:",
|
||||
"FROM": "Afsender",
|
||||
"IN": "Indbakke",
|
||||
"AGENTS": "Agenter",
|
||||
"CONTACTS": "Kontakter",
|
||||
"INBOXES": "Indbakker",
|
||||
"NO_AGENTS": "Ingen agenter fundet",
|
||||
"NO_CONTACTS": "Start by searching to see results",
|
||||
"NO_INBOXES": "No inboxes found"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"OPTIONS": {
|
||||
"NAME": "Name",
|
||||
"DOMAIN": "Domain",
|
||||
"CREATED_AT": "Erstellt am"
|
||||
"CREATED_AT": "Erstellt am",
|
||||
"CONTACTS_COUNT": "Contacts count"
|
||||
}
|
||||
},
|
||||
"ORDER": {
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"TYPES": {
|
||||
"MEDIA": "Media",
|
||||
"QUICK_REPLY": "Quick Reply",
|
||||
"CALL_TO_ACTION": "Call to Action",
|
||||
"TEXT": "Text"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -275,6 +275,16 @@
|
||||
"SIDEBAR": {
|
||||
"CONTACT": "Kontakt",
|
||||
"COPILOT": "Copilot"
|
||||
},
|
||||
"VOICE_WIDGET": {
|
||||
"INCOMING_CALL": "Eingehender Anruf",
|
||||
"OUTGOING_CALL": "Ausgehender Anruf",
|
||||
"CALL_IN_PROGRESS": "Anruf läuft",
|
||||
"NOT_ANSWERED_YET": "Noch nicht beantwortet",
|
||||
"HANDLED_IN_ANOTHER_TAB": "Being handled in another tab",
|
||||
"REJECT_CALL": "Reject",
|
||||
"JOIN_CALL": "Join call",
|
||||
"END_CALL": "End call"
|
||||
}
|
||||
},
|
||||
"EMAIL_TRANSCRIPT": {
|
||||
|
||||
@@ -808,6 +808,35 @@
|
||||
"LABEL": "Nachricht",
|
||||
"PLACEHOLDER": "Please enter a message to show users with the form"
|
||||
},
|
||||
"BUTTON_TEXT": {
|
||||
"LABEL": "Button text",
|
||||
"PLACEHOLDER": "Please rate us"
|
||||
},
|
||||
"LANGUAGE": {
|
||||
"LABEL": "Sprache",
|
||||
"PLACEHOLDER": "Select template language"
|
||||
},
|
||||
"MESSAGE_PREVIEW": {
|
||||
"LABEL": "Message preview",
|
||||
"TOOLTIP": "This may vary slightly when rendered on WhatsApp's platform."
|
||||
},
|
||||
"TEMPLATE_STATUS": {
|
||||
"APPROVED": "Approved by WhatsApp",
|
||||
"PENDING": "Pending WhatsApp approval",
|
||||
"REJECTED": "Meta rejected the template",
|
||||
"DEFAULT": "Needs WhatsApp approval",
|
||||
"NOT_FOUND": "The template does not exist in the Meta platform."
|
||||
},
|
||||
"TEMPLATE_CREATION": {
|
||||
"SUCCESS_MESSAGE": "WhatsApp template created successfully and sent for approval",
|
||||
"ERROR_MESSAGE": "Failed to create WhatsApp template"
|
||||
},
|
||||
"TEMPLATE_UPDATE_DIALOG": {
|
||||
"TITLE": "Edit survey details",
|
||||
"DESCRIPTION": "We will delete the previous template and make a new one which will be sent again for WhatsApp approval",
|
||||
"CONFIRM": "Create new template",
|
||||
"CANCEL": "Zurück"
|
||||
},
|
||||
"SURVEY_RULE": {
|
||||
"LABEL": "Survey rule",
|
||||
"DESCRIPTION_PREFIX": "Send the survey if the conversation",
|
||||
@@ -819,6 +848,7 @@
|
||||
"SELECT_PLACEHOLDER": "select labels"
|
||||
},
|
||||
"NOTE": "Note: CSAT surveys are sent only once per conversation",
|
||||
"WHATSAPP_NOTE": "Note: We will create a template and send it for WhatsApp approval. After being approved, surveys will be sent only once per conversation as per the survey rule.",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "CSAT settings updated successfully",
|
||||
"ERROR_MESSAGE": "We couldn't update CSAT settings. Please try again later."
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"SEARCH": {
|
||||
"TABS": {
|
||||
"ALL": "Alle",
|
||||
"ALL": "All results",
|
||||
"CONTACTS": "Kontakte",
|
||||
"CONVERSATIONS": "Gespräche",
|
||||
"MESSAGES": "Nachrichten",
|
||||
@@ -19,14 +19,50 @@
|
||||
"LOADING_DATA": "Wird geladen",
|
||||
"EMPTY_STATE": "Keine {item} für Abfrage '{query} ' gefunden",
|
||||
"EMPTY_STATE_FULL": "Kein Ergebnis für Abfrage '{query} ' gefunden",
|
||||
"PLACEHOLDER_KEYBINDING": "/ fokussieren",
|
||||
"PLACEHOLDER_KEYBINDING": "/fokussieren",
|
||||
"INPUT_PLACEHOLDER": "Search messages, contacts or conversations",
|
||||
"RECENT_SEARCHES": "Recent searches",
|
||||
"CLEAR_ALL": "Alle löschen",
|
||||
"MOST_RECENT": "Most recent",
|
||||
"EMPTY_STATE_DEFAULT": "Search by conversation id, email, phone number, messages for better search results.",
|
||||
"BOT_LABEL": "Bot",
|
||||
"READ_MORE": "Mehr erfahren",
|
||||
"READ_LESS": "Read less",
|
||||
"WROTE": "schrieb:",
|
||||
"FROM": "von",
|
||||
"EMAIL": "e-Mail",
|
||||
"EMAIL_SUBJECT": "betreff"
|
||||
"FROM": "Von",
|
||||
"EMAIL": "E-Mail",
|
||||
"EMAIL_SUBJECT": "Betreff",
|
||||
"PRIVATE": "Private note",
|
||||
"TRANSCRIPT": "Transcript",
|
||||
"CREATED_AT": "erstellt am {time}",
|
||||
"UPDATED_AT": "updated {time}",
|
||||
"SORT_BY": {
|
||||
"RELEVANCE": "Relevance"
|
||||
},
|
||||
"DATE_RANGE": {
|
||||
"LAST_7_DAYS": "Letzte 7 Tage",
|
||||
"LAST_30_DAYS": "Letzte 30 Tage",
|
||||
"LAST_60_DAYS": "Letzten 60 Tage",
|
||||
"LAST_90_DAYS": "Letzten 90 Tage",
|
||||
"CUSTOM_RANGE": "Custom range:",
|
||||
"CREATED_BETWEEN": "Created between",
|
||||
"AND": "und",
|
||||
"APPLY": "Übernehmen",
|
||||
"BEFORE_DATE": "Before {date}",
|
||||
"AFTER_DATE": "After {date}",
|
||||
"TIME_RANGE": "Filter by time",
|
||||
"CLEAR_FILTER": "Filter löschen"
|
||||
},
|
||||
"FILTERS": {
|
||||
"FILTER_MESSAGE": "Filter messages by:",
|
||||
"FROM": "Absender",
|
||||
"IN": "Posteingang",
|
||||
"AGENTS": "Agenten",
|
||||
"CONTACTS": "Kontakte",
|
||||
"INBOXES": "Posteingänge",
|
||||
"NO_AGENTS": "Keine Agenten gefunden",
|
||||
"NO_CONTACTS": "Start by searching to see results",
|
||||
"NO_INBOXES": "No inboxes found"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"OPTIONS": {
|
||||
"NAME": "Όνομα",
|
||||
"DOMAIN": "Domain",
|
||||
"CREATED_AT": "Δημιουργήθηκε στις"
|
||||
"CREATED_AT": "Δημιουργήθηκε στις",
|
||||
"CONTACTS_COUNT": "Contacts count"
|
||||
}
|
||||
},
|
||||
"ORDER": {
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"TYPES": {
|
||||
"MEDIA": "Media",
|
||||
"QUICK_REPLY": "Quick Reply",
|
||||
"CALL_TO_ACTION": "Call to Action",
|
||||
"TEXT": "Κείμενο"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -275,6 +275,16 @@
|
||||
"SIDEBAR": {
|
||||
"CONTACT": "Επαφές",
|
||||
"COPILOT": "Copilot"
|
||||
},
|
||||
"VOICE_WIDGET": {
|
||||
"INCOMING_CALL": "Incoming call",
|
||||
"OUTGOING_CALL": "Outgoing call",
|
||||
"CALL_IN_PROGRESS": "Call in progress",
|
||||
"NOT_ANSWERED_YET": "Not answered yet",
|
||||
"HANDLED_IN_ANOTHER_TAB": "Being handled in another tab",
|
||||
"REJECT_CALL": "Reject",
|
||||
"JOIN_CALL": "Join call",
|
||||
"END_CALL": "End call"
|
||||
}
|
||||
},
|
||||
"EMAIL_TRANSCRIPT": {
|
||||
|
||||
@@ -808,6 +808,35 @@
|
||||
"LABEL": "Μήνυμα",
|
||||
"PLACEHOLDER": "Please enter a message to show users with the form"
|
||||
},
|
||||
"BUTTON_TEXT": {
|
||||
"LABEL": "Button text",
|
||||
"PLACEHOLDER": "Please rate us"
|
||||
},
|
||||
"LANGUAGE": {
|
||||
"LABEL": "Γλώσσα",
|
||||
"PLACEHOLDER": "Select template language"
|
||||
},
|
||||
"MESSAGE_PREVIEW": {
|
||||
"LABEL": "Message preview",
|
||||
"TOOLTIP": "This may vary slightly when rendered on WhatsApp's platform."
|
||||
},
|
||||
"TEMPLATE_STATUS": {
|
||||
"APPROVED": "Approved by WhatsApp",
|
||||
"PENDING": "Pending WhatsApp approval",
|
||||
"REJECTED": "Meta rejected the template",
|
||||
"DEFAULT": "Needs WhatsApp approval",
|
||||
"NOT_FOUND": "The template does not exist in the Meta platform."
|
||||
},
|
||||
"TEMPLATE_CREATION": {
|
||||
"SUCCESS_MESSAGE": "WhatsApp template created successfully and sent for approval",
|
||||
"ERROR_MESSAGE": "Failed to create WhatsApp template"
|
||||
},
|
||||
"TEMPLATE_UPDATE_DIALOG": {
|
||||
"TITLE": "Edit survey details",
|
||||
"DESCRIPTION": "We will delete the previous template and make a new one which will be sent again for WhatsApp approval",
|
||||
"CONFIRM": "Create new template",
|
||||
"CANCEL": "Πίσω"
|
||||
},
|
||||
"SURVEY_RULE": {
|
||||
"LABEL": "Survey rule",
|
||||
"DESCRIPTION_PREFIX": "Send the survey if the conversation",
|
||||
@@ -819,6 +848,7 @@
|
||||
"SELECT_PLACEHOLDER": "select labels"
|
||||
},
|
||||
"NOTE": "Note: CSAT surveys are sent only once per conversation",
|
||||
"WHATSAPP_NOTE": "Note: We will create a template and send it for WhatsApp approval. After being approved, surveys will be sent only once per conversation as per the survey rule.",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "CSAT settings updated successfully",
|
||||
"ERROR_MESSAGE": "We couldn't update CSAT settings. Please try again later."
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user