Compare commits
92
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9def5f80a3 | ||
|
|
a8c8b38f51 | ||
|
|
f422c83c26 | ||
|
|
722e68eecb | ||
|
|
0592cccca9 | ||
|
|
45b6ea6b3f | ||
|
|
de0bd8e71b | ||
|
|
224b1f98b0 | ||
|
|
3190b29fe9 | ||
|
|
42163946eb | ||
|
|
f13f3ba446 | ||
|
|
f1da7b8afa | ||
|
|
bd14e96ed9 | ||
|
|
00837019b5 | ||
|
|
45124c3b41 | ||
|
|
699b12b1d3 | ||
|
|
e5107604a0 | ||
|
|
871f2f4d56 | ||
|
|
d92b278b60 | ||
|
|
d26b6e0e64 | ||
|
|
4f94ad4a75 | ||
|
|
fbe3560b7a | ||
|
|
118270d2e8 | ||
|
|
8c0c0fd32c | ||
|
|
50d6ebaaca | ||
|
|
95463230cb | ||
|
|
dd641aeef3 | ||
|
|
f4d66566d0 | ||
|
|
9119e947f0 | ||
|
|
5fd3d5e036 | ||
|
|
ac60784f95 | ||
|
|
9ab45ad50e | ||
|
|
1e3be0ac24 | ||
|
|
35715ae841 | ||
|
|
27fd2b8d86 | ||
|
|
6f5ad8f372 | ||
|
|
441fe4db11 | ||
|
|
b9b5a18767 | ||
|
|
b815eb9ce0 | ||
|
|
b3d0af84c4 | ||
|
|
d83beb2148 | ||
|
|
c71bcc2428 | ||
|
|
8daf6cf6cb | ||
|
|
211fb1102d | ||
|
|
7b09b033ef | ||
|
|
65867b8b36 | ||
|
|
4cce7f6ad8 | ||
|
|
f2cb23d6e9 | ||
|
|
8824efe0e1 | ||
|
|
5de7ae492c | ||
|
|
b5db3c9512 | ||
|
|
b4b5de9b46 | ||
|
|
1987ac3d97 | ||
|
|
0012fa2c35 | ||
|
|
b4ce59eea8 | ||
|
|
42441dbd28 | ||
|
|
b9f824b43b | ||
|
|
7651c18b48 | ||
|
|
04acc16609 | ||
|
|
44a7a13117 | ||
|
|
9efd554693 | ||
|
|
2b296c06fb | ||
|
|
4381be5f3e | ||
|
|
127ac0a6b2 | ||
|
|
5d9d754961 | ||
|
|
cac7438fff | ||
|
|
0b41d7f483 | ||
|
|
4517c50227 | ||
|
|
4c4b70da25 | ||
|
|
d84ef4cfd6 | ||
|
|
23786bcb52 | ||
|
|
e4c3f0ac2f | ||
|
|
742c5cc1f4 | ||
|
|
d9e732c005 | ||
|
|
e0e321b8e2 | ||
|
|
ecc66e064d | ||
|
|
7144d55334 | ||
|
|
250650dd7a | ||
|
|
608be1036b | ||
|
|
6ff643b045 | ||
|
|
775b73d1f9 | ||
|
|
14df7b3bc1 | ||
|
|
6946859ba4 | ||
|
|
c129ab00ba | ||
|
|
7edae93ee8 | ||
|
|
4b315bc2ec | ||
|
|
30c0479e9a | ||
|
|
3c0d55f87a | ||
|
|
4af3e830fc | ||
|
|
b974993886 | ||
|
|
4b849cdd11 | ||
|
|
310590cae3 |
@@ -0,0 +1,65 @@
|
||||
---
|
||||
:position: before
|
||||
:position_in_additional_file_patterns: before
|
||||
:position_in_class: before
|
||||
:position_in_factory: before
|
||||
:position_in_fixture: before
|
||||
:position_in_routes: before
|
||||
:position_in_serializer: before
|
||||
:position_in_test: before
|
||||
:classified_sort: true
|
||||
:exclude_controllers: true
|
||||
:exclude_factories: true
|
||||
:exclude_fixtures: true
|
||||
:exclude_helpers: true
|
||||
:exclude_scaffolds: true
|
||||
:exclude_serializers: true
|
||||
:exclude_sti_subclasses: false
|
||||
:exclude_tests: true
|
||||
:force: false
|
||||
:format_markdown: false
|
||||
:format_rdoc: false
|
||||
:format_yard: false
|
||||
:frozen: false
|
||||
:grouped_polymorphic: false
|
||||
:ignore_model_sub_dir: false
|
||||
:ignore_unknown_models: false
|
||||
:include_version: false
|
||||
:show_check_constraints: false
|
||||
:show_complete_foreign_keys: false
|
||||
:show_foreign_keys: true
|
||||
:show_indexes: true
|
||||
:show_indexes_include: false
|
||||
:simple_indexes: false
|
||||
:sort: false
|
||||
:timestamp: false
|
||||
:trace: false
|
||||
:with_comment: true
|
||||
:with_column_comments: true
|
||||
:with_table_comments: true
|
||||
:position_of_column_comment: :with_name
|
||||
:active_admin: false
|
||||
:command:
|
||||
:debug: false
|
||||
:hide_default_column_types: json,jsonb,hstore
|
||||
:hide_limit_column_types: integer,bigint,boolean
|
||||
:timestamp_columns:
|
||||
- created_at
|
||||
- updated_at
|
||||
:ignore_columns:
|
||||
:ignore_routes:
|
||||
:models: true
|
||||
:routes: false
|
||||
:skip_on_db_migrate: false
|
||||
:target_action: :do_annotations
|
||||
:wrapper:
|
||||
:wrapper_close:
|
||||
:wrapper_open:
|
||||
:classes_default_to_s: []
|
||||
:additional_file_patterns: []
|
||||
:model_dir:
|
||||
- app/models
|
||||
- enterprise/app/models
|
||||
:require: []
|
||||
:root_dir:
|
||||
- ''
|
||||
@@ -2,3 +2,8 @@
|
||||
ignore:
|
||||
- CVE-2021-41098 # https://github.com/chatwoot/chatwoot/issues/3097 (update once azure blob storage is updated)
|
||||
- GHSA-57hq-95w6-v4fc # Devise confirmable race condition — patched locally in User model (remove once on Devise 5+)
|
||||
# Chatwoot defaults to Active Storage redirect-style URLs, and its recommended
|
||||
# storage setup uses local/cloud storage with optional direct uploads to the
|
||||
# storage provider rather than Rails proxy mode. Revisit if we enable
|
||||
# rails_storage_proxy or other app-served Active Storage proxy routes.
|
||||
- CVE-2026-33658
|
||||
|
||||
@@ -94,10 +94,6 @@ yarn-debug.log*
|
||||
.vscode
|
||||
.claude/settings.local.json
|
||||
.cursor
|
||||
|
||||
# react component
|
||||
dist
|
||||
|
||||
.codex/
|
||||
.claude/
|
||||
CLAUDE.local.md
|
||||
|
||||
@@ -40,6 +40,8 @@ gem 'json_refs'
|
||||
gem 'rack-attack', '>= 6.7.0'
|
||||
# a utility tool for streaming, flexible and safe downloading of remote files
|
||||
gem 'down'
|
||||
# SSRF-safe URL fetching
|
||||
gem 'ssrf_filter', '~> 1.5'
|
||||
# authentication type to fetch and send mail over oauth2.0
|
||||
gem 'gmail_xoauth'
|
||||
# Lock net-smtp to 0.3.4 to avoid issues with gmail_xoauth2
|
||||
|
||||
+4
-2
@@ -166,7 +166,7 @@ GEM
|
||||
multi_json (~> 1)
|
||||
statsd-ruby (~> 1.1)
|
||||
base64 (0.3.0)
|
||||
bcrypt (3.1.20)
|
||||
bcrypt (3.1.22)
|
||||
benchmark (0.4.1)
|
||||
bigdecimal (3.2.2)
|
||||
bindex (0.8.1)
|
||||
@@ -465,7 +465,7 @@ GEM
|
||||
rails-dom-testing (>= 1, < 3)
|
||||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
json (2.18.1)
|
||||
json (2.19.2)
|
||||
json_refs (0.1.8)
|
||||
hana
|
||||
json_schemer (0.2.24)
|
||||
@@ -942,6 +942,7 @@ GEM
|
||||
activesupport (>= 5.2)
|
||||
sprockets (>= 3.0.0)
|
||||
squasher (0.7.2)
|
||||
ssrf_filter (1.5.0)
|
||||
stackprof (0.2.25)
|
||||
statsd-ruby (1.5.0)
|
||||
stripe (18.0.1)
|
||||
@@ -1158,6 +1159,7 @@ DEPENDENCIES
|
||||
spring
|
||||
spring-watcher-listen
|
||||
squasher
|
||||
ssrf_filter (~> 1.5)
|
||||
stackprof
|
||||
stripe (~> 18.0)
|
||||
telephone_number
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
4.12.0
|
||||
4.12.1
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
class Email::BaseBuilder
|
||||
include EmailAddressParseable
|
||||
|
||||
pattr_initialize [:inbox!]
|
||||
|
||||
private
|
||||
@@ -47,8 +49,4 @@ class Email::BaseBuilder
|
||||
# can save it in the format "Name <email@domain.com>"
|
||||
parse_email(account.support_email)
|
||||
end
|
||||
|
||||
def parse_email(email_string)
|
||||
Mail::Address.new(email_string).address
|
||||
end
|
||||
end
|
||||
|
||||
@@ -34,6 +34,10 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController
|
||||
@agent_bot.reload
|
||||
end
|
||||
|
||||
def reset_secret
|
||||
@agent_bot.reset_secret!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def agent_bot
|
||||
|
||||
+3
-1
@@ -57,7 +57,7 @@ class Api::V1::Accounts::Captain::TasksController < Api::V1::Accounts::BaseContr
|
||||
if result.nil?
|
||||
render json: { message: nil }
|
||||
elsif result[:error]
|
||||
render json: { error: result[:error] }, status: :unprocessable_entity
|
||||
render json: { error: result[:error] }, status: :unprocessable_content
|
||||
else
|
||||
response_data = { message: result[:message] }
|
||||
response_data[:follow_up_context] = result[:follow_up_context] if result[:follow_up_context]
|
||||
@@ -69,3 +69,5 @@ class Api::V1::Accounts::Captain::TasksController < Api::V1::Accounts::BaseContr
|
||||
authorize(:'captain/tasks')
|
||||
end
|
||||
end
|
||||
|
||||
Api::V1::Accounts::Captain::TasksController.prepend_mod_with('Api::V1::Accounts::Captain::TasksController')
|
||||
@@ -116,6 +116,8 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
# High-traffic accounts generate excessive DB writes when agents frequently switch between conversations.
|
||||
# Throttle last_seen updates to once per hour when there are no unread messages to reduce DB load.
|
||||
# Always update immediately if there are unread messages to maintain accurate read/unread state.
|
||||
# Visiting a conversation should clear any unread inbox notifications for this conversation.
|
||||
Notification::MarkConversationReadService.new(user: Current.user, account: Current.account, conversation: @conversation).perform
|
||||
return update_last_seen_on_conversation(DateTime.now.utc, true) if assignee? && @conversation.assignee_unread_messages.any?
|
||||
return update_last_seen_on_conversation(DateTime.now.utc, false) if !assignee? && @conversation.unread_messages.any?
|
||||
|
||||
|
||||
@@ -66,6 +66,12 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
head :ok
|
||||
end
|
||||
|
||||
def reset_secret
|
||||
return head :not_found unless @inbox.api?
|
||||
|
||||
@inbox.channel.reset_secret!
|
||||
end
|
||||
|
||||
def destroy
|
||||
::DeleteObjectJob.perform_later(@inbox, Current.user, request.ip) if @inbox.present?
|
||||
render status: :ok, json: { message: I18n.t('messages.inbox_deletetion_response') }
|
||||
|
||||
@@ -5,7 +5,7 @@ class Api::V1::Accounts::UploadController < Api::V1::Accounts::BaseController
|
||||
elsif params[:external_url].present?
|
||||
create_from_url
|
||||
else
|
||||
render_error('No file or URL provided', :unprocessable_entity)
|
||||
render_error(I18n.t('errors.upload.missing_input'), :unprocessable_entity)
|
||||
end
|
||||
|
||||
render_success(result) if result.is_a?(ActiveStorage::Blob)
|
||||
@@ -19,35 +19,21 @@ class Api::V1::Accounts::UploadController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def create_from_url
|
||||
uri = parse_uri(params[:external_url])
|
||||
return if performed?
|
||||
|
||||
fetch_and_process_file_from_uri(uri)
|
||||
end
|
||||
|
||||
def parse_uri(url)
|
||||
uri = URI.parse(url)
|
||||
validate_uri(uri)
|
||||
uri
|
||||
rescue URI::InvalidURIError, SocketError
|
||||
render_error('Invalid URL provided', :unprocessable_entity)
|
||||
nil
|
||||
end
|
||||
|
||||
def validate_uri(uri)
|
||||
raise URI::InvalidURIError unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
|
||||
end
|
||||
|
||||
def fetch_and_process_file_from_uri(uri)
|
||||
uri.open do |file|
|
||||
create_and_save_blob(file, File.basename(uri.path), file.content_type)
|
||||
SafeFetch.fetch(params[:external_url].to_s) do |result|
|
||||
create_and_save_blob(result.tempfile, result.filename, result.content_type)
|
||||
end
|
||||
rescue OpenURI::HTTPError => e
|
||||
render_error("Failed to fetch file from URL: #{e.message}", :unprocessable_entity)
|
||||
rescue SocketError
|
||||
render_error('Invalid URL provided', :unprocessable_entity)
|
||||
rescue SafeFetch::HttpError => e
|
||||
render_error(I18n.t('errors.upload.fetch_failed_with_message', message: e.message), :unprocessable_entity)
|
||||
rescue SafeFetch::FetchError
|
||||
render_error(I18n.t('errors.upload.fetch_failed'), :unprocessable_entity)
|
||||
rescue SafeFetch::FileTooLargeError
|
||||
render_error(I18n.t('errors.upload.file_too_large'), :unprocessable_entity)
|
||||
rescue SafeFetch::UnsupportedContentTypeError
|
||||
render_error(I18n.t('errors.upload.unsupported_content_type'), :unprocessable_entity)
|
||||
rescue SafeFetch::Error
|
||||
render_error(I18n.t('errors.upload.invalid_url'), :unprocessable_entity)
|
||||
rescue StandardError
|
||||
render_error('An unexpected error occurred', :internal_server_error)
|
||||
render_error(I18n.t('errors.upload.unexpected'), :internal_server_error)
|
||||
end
|
||||
|
||||
def create_and_save_blob(io, filename, content_type)
|
||||
|
||||
@@ -30,9 +30,20 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
locale: account_params[:locale],
|
||||
user: current_user
|
||||
).perform
|
||||
enqueue_branding_enrichment
|
||||
if @user
|
||||
send_auth_headers(@user)
|
||||
render 'api/v1/accounts/create', format: :json, locals: { resource: @user }
|
||||
# Authenticated users (dashboard "add account") and api_only signups
|
||||
# need the full response with account_id. API-only deployments have no
|
||||
# frontend to handle the email confirmation flow, so they need auth
|
||||
# tokens to proceed.
|
||||
# Unauthenticated web signup returns only the email — no session is
|
||||
# created until the user confirms via the email link.
|
||||
if current_user || api_only_signup?
|
||||
send_auth_headers(@user)
|
||||
render 'api/v1/accounts/create', format: :json, locals: { resource: @user }
|
||||
else
|
||||
render json: { email: @user.email }
|
||||
end
|
||||
else
|
||||
render_error_response(CustomExceptions::Account::SignupFailed.new({}))
|
||||
end
|
||||
@@ -59,6 +70,16 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
|
||||
private
|
||||
|
||||
def enqueue_branding_enrichment
|
||||
return if account_params[:email].blank?
|
||||
|
||||
Account::BrandingEnrichmentJob.perform_later(@account.id, account_params[:email])
|
||||
Redis::Alfred.set(format(Redis::Alfred::ACCOUNT_ONBOARDING_ENRICHMENT, account_id: @account.id), '1', ex: 30)
|
||||
rescue StandardError => e
|
||||
# Enrichment is optional — never let queue/Redis failures abort signup
|
||||
ChatwootExceptionTracker.new(e).capture_exception
|
||||
end
|
||||
|
||||
def ensure_account_name
|
||||
# ensure that account_name and user_full_name is present
|
||||
# this is becuase the account builder and the models validations are not triggered
|
||||
@@ -103,6 +124,15 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
raise ActionController::RoutingError, 'Not Found' unless GlobalConfigService.account_signup_enabled?
|
||||
end
|
||||
|
||||
def api_only_signup?
|
||||
# CW_API_ONLY_SERVER is the canonical flag for API-only deployments.
|
||||
# ENABLE_ACCOUNT_SIGNUP='api_only' is a legacy sentinel for the same purpose.
|
||||
# Read ENABLE_ACCOUNT_SIGNUP raw from InstallationConfig because GlobalConfig.get
|
||||
# typecasts it to boolean, coercing 'api_only' to true.
|
||||
ActiveModel::Type::Boolean.new.cast(ENV.fetch('CW_API_ONLY_SERVER', false)) ||
|
||||
InstallationConfig.find_by(name: 'ENABLE_ACCOUNT_SIGNUP')&.value.to_s == 'api_only'
|
||||
end
|
||||
|
||||
def validate_captcha
|
||||
raise ActionController::InvalidAuthenticityToken, 'Invalid Captcha' unless ChatwootCaptcha.new(params[:h_captcha_client_response]).valid?
|
||||
end
|
||||
|
||||
@@ -43,7 +43,15 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||
end
|
||||
|
||||
def set_conversation
|
||||
@conversation = create_conversation if conversation.nil?
|
||||
return unless conversation.nil?
|
||||
|
||||
@conversation = create_conversation
|
||||
apply_labels if permitted_params[:labels].present?
|
||||
end
|
||||
|
||||
def apply_labels
|
||||
valid_labels = inbox.account.labels.where(title: permitted_params[:labels]).pluck(:title)
|
||||
@conversation.update_labels(valid_labels) if valid_labels.present?
|
||||
end
|
||||
|
||||
def message_finder_params
|
||||
@@ -64,7 +72,14 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||
|
||||
def permitted_params
|
||||
# timestamp parameter is used in create conversation method
|
||||
params.permit(:id, :before, :after, :website_token, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id, :reply_to])
|
||||
# custom_attributes and labels are applied when a new conversation is created alongside the first message
|
||||
params.permit(
|
||||
:id, :before, :after, :website_token,
|
||||
contact: [:name, :email],
|
||||
message: [:content, :referer_url, :timestamp, :echo_id, :reply_to],
|
||||
custom_attributes: {},
|
||||
labels: []
|
||||
)
|
||||
end
|
||||
|
||||
def set_message
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Unauthenticated endpoint for resending confirmation emails during signup.
|
||||
# This is a standalone controller (not on DeviseOverrides::ConfirmationsController)
|
||||
# because OmniAuth middleware intercepts all POST /auth/* routes as provider
|
||||
# callbacks, and Devise controller filters cause 307 redirects for custom actions.
|
||||
# Inherits from ActionController::API to avoid both issues entirely.
|
||||
# Rate-limited by Rack::Attack (IP + email) and gated by hCaptcha.
|
||||
class Auth::ResendConfirmationsController < ActionController::API
|
||||
def create
|
||||
return head(:ok) unless ChatwootCaptcha.new(params[:h_captcha_client_response]).valid?
|
||||
|
||||
email = params[:email]
|
||||
return head(:ok) unless email.is_a?(String)
|
||||
|
||||
user = User.from_email(email.strip.downcase)
|
||||
user&.send_confirmation_instructions unless user&.confirmed?
|
||||
head :ok
|
||||
end
|
||||
end
|
||||
@@ -10,7 +10,12 @@ class DeviseOverrides::OmniauthCallbacksController < DeviseTokenAuth::OmniauthCa
|
||||
private
|
||||
|
||||
def sign_in_user
|
||||
# Capture before skip_confirmation! sets confirmed_at, which would
|
||||
# make oauth_user_needs_password_reset? return false and skip the
|
||||
# password reset for persisted unconfirmed users.
|
||||
needs_password_reset = oauth_user_needs_password_reset?
|
||||
@resource.skip_confirmation! if confirmable_enabled?
|
||||
set_random_password_if_oauth_user if needs_password_reset
|
||||
|
||||
# once the resource is found and verified
|
||||
# we can just send them to the login page again with the SSO params
|
||||
@@ -20,7 +25,10 @@ class DeviseOverrides::OmniauthCallbacksController < DeviseTokenAuth::OmniauthCa
|
||||
end
|
||||
|
||||
def sign_in_user_on_mobile
|
||||
# See comment in sign_in_user for why this is captured before skip_confirmation!
|
||||
needs_password_reset = oauth_user_needs_password_reset?
|
||||
@resource.skip_confirmation! if confirmable_enabled?
|
||||
set_random_password_if_oauth_user if needs_password_reset
|
||||
|
||||
# once the resource is found and verified
|
||||
# we can just send them to the login page again with the SSO params
|
||||
@@ -37,6 +45,7 @@ class DeviseOverrides::OmniauthCallbacksController < DeviseTokenAuth::OmniauthCa
|
||||
return redirect_to login_page_url(error: 'business-account-only') unless validate_signup_email_is_business_domain?
|
||||
|
||||
create_account_for_user
|
||||
set_random_password_if_oauth_user
|
||||
token = @resource.send(:set_reset_password_token)
|
||||
frontend_url = ENV.fetch('FRONTEND_URL', nil)
|
||||
redirect_to "#{frontend_url}/app/auth/password/edit?config=default&reset_password_token=#{token}"
|
||||
@@ -81,6 +90,15 @@ class DeviseOverrides::OmniauthCallbacksController < DeviseTokenAuth::OmniauthCa
|
||||
Avatar::AvatarFromUrlJob.perform_later(@resource, auth_hash['info']['image'])
|
||||
end
|
||||
|
||||
def oauth_user_needs_password_reset?
|
||||
@resource.present? && (@resource.new_record? || !@resource.confirmed?)
|
||||
end
|
||||
|
||||
def set_random_password_if_oauth_user
|
||||
# Password must satisfy secure_password requirements (uppercase, lowercase, number, special char)
|
||||
@resource.update(password: "#{SecureRandom.hex(16)}aA1!") if @resource.persisted?
|
||||
end
|
||||
|
||||
def default_devise_mapping
|
||||
'user'
|
||||
end
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
class Platform::Api::V1::EmailChannelMigrationsController < PlatformController
|
||||
before_action :set_account
|
||||
before_action :validate_account_permissible
|
||||
before_action :validate_feature_flag
|
||||
before_action :validate_params
|
||||
|
||||
def create
|
||||
results = migrate_email_channels
|
||||
render json: { results: results }, status: :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:account_id])
|
||||
end
|
||||
|
||||
def validate_account_permissible
|
||||
return if @platform_app.platform_app_permissibles.find_by(permissible: @account)
|
||||
|
||||
render json: { error: 'Non permissible resource' }, status: :unauthorized
|
||||
end
|
||||
|
||||
def validate_feature_flag
|
||||
return if ActiveModel::Type::Boolean.new.cast(ENV.fetch('EMAIL_CHANNEL_MIGRATION', false))
|
||||
|
||||
render json: { error: 'Email channel migration is not enabled' }, status: :forbidden
|
||||
end
|
||||
|
||||
def validate_params
|
||||
return render json: { error: 'Missing migrations parameter' }, status: :unprocessable_entity if migration_params.blank?
|
||||
|
||||
return unless migration_params.size > MAX_MIGRATIONS
|
||||
|
||||
return render json: { error: "Too many migrations (max #{MAX_MIGRATIONS})" },
|
||||
status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def migrate_email_channels
|
||||
migration_params.map { |entry| migrate_single(entry) }
|
||||
end
|
||||
|
||||
MAX_MIGRATIONS = 25
|
||||
SUPPORTED_PROVIDERS = %w[google microsoft].freeze
|
||||
|
||||
def migrate_single(entry)
|
||||
validate_provider!(entry[:provider])
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
channel = create_channel(entry)
|
||||
inbox = create_inbox(channel, entry)
|
||||
|
||||
{ email: entry[:email], inbox_id: inbox.id, channel_id: channel.id, status: 'success' }
|
||||
end
|
||||
rescue StandardError => e
|
||||
{ email: entry[:email], status: 'error', message: e.message }
|
||||
end
|
||||
|
||||
def create_channel(entry)
|
||||
Channel::Email.create!(
|
||||
account_id: @account.id,
|
||||
email: entry[:email],
|
||||
provider: entry[:provider],
|
||||
provider_config: entry[:provider_config]&.to_h,
|
||||
imap_enabled: entry.fetch(:imap_enabled, true),
|
||||
imap_address: entry[:imap_address] || default_imap_address(entry[:provider]),
|
||||
imap_port: entry[:imap_port] || 993,
|
||||
imap_login: entry[:imap_login] || entry[:email],
|
||||
imap_enable_ssl: entry.fetch(:imap_enable_ssl, true)
|
||||
)
|
||||
end
|
||||
|
||||
def create_inbox(channel, entry)
|
||||
@account.inboxes.create!(
|
||||
name: entry[:inbox_name] || "Migrated #{entry[:provider]&.capitalize}: #{entry[:email]}",
|
||||
channel: channel
|
||||
)
|
||||
end
|
||||
|
||||
def validate_provider!(provider)
|
||||
return if SUPPORTED_PROVIDERS.include?(provider)
|
||||
|
||||
raise ArgumentError, "Unsupported provider '#{provider}'. Must be one of: #{SUPPORTED_PROVIDERS.join(', ')}"
|
||||
end
|
||||
|
||||
def default_imap_address(provider)
|
||||
case provider
|
||||
when 'google' then 'imap.gmail.com'
|
||||
when 'microsoft' then 'outlook.office365.com'
|
||||
else ''
|
||||
end
|
||||
end
|
||||
|
||||
def migration_params
|
||||
params.permit(migrations: [
|
||||
:email, :provider, :inbox_name,
|
||||
:imap_enabled, :imap_address, :imap_port, :imap_login, :imap_enable_ssl,
|
||||
{ provider_config: {} }
|
||||
])[:migrations]
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,7 @@
|
||||
class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::BaseController
|
||||
before_action :ensure_custom_domain_request, only: [:show, :index]
|
||||
before_action :portal
|
||||
before_action :ensure_portal_feature_enabled
|
||||
before_action :set_category, except: [:index, :show, :tracking_pixel]
|
||||
before_action :set_article, only: [:show]
|
||||
layout 'portal'
|
||||
@@ -61,7 +62,7 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
|
||||
|
||||
def set_article
|
||||
@article = @portal.articles.find_by(slug: permitted_params[:article_slug])
|
||||
@parsed_content = render_article_content(@article.content)
|
||||
@parsed_content = render_article_content(@article.content.to_s)
|
||||
end
|
||||
|
||||
def set_category
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
class Public::Api::V1::Portals::CategoriesController < Public::Api::V1::Portals::BaseController
|
||||
before_action :ensure_custom_domain_request, only: [:show, :index]
|
||||
before_action :portal
|
||||
before_action :ensure_portal_feature_enabled
|
||||
before_action :set_category, only: [:show]
|
||||
layout 'portal'
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
class Public::Api::V1::PortalsController < Public::Api::V1::Portals::BaseController
|
||||
before_action :ensure_custom_domain_request, only: [:show]
|
||||
before_action :portal
|
||||
before_action :redirect_to_portal_with_locale, only: [:show]
|
||||
before_action :portal
|
||||
before_action :ensure_portal_feature_enabled
|
||||
layout 'portal'
|
||||
|
||||
def show
|
||||
@@ -24,6 +25,7 @@ class Public::Api::V1::PortalsController < Public::Api::V1::Portals::BaseControl
|
||||
def redirect_to_portal_with_locale
|
||||
return if params[:locale].present?
|
||||
|
||||
portal
|
||||
redirect_to "/hc/#{@portal.slug}/#{@portal.default_locale}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,4 +18,11 @@ class PublicController < ActionController::Base
|
||||
Please send us an email at support@chatwoot.com with the custom domain name and account API key"
|
||||
}, status: :unauthorized and return
|
||||
end
|
||||
|
||||
def ensure_portal_feature_enabled
|
||||
return unless ChatwootApp.chatwoot_cloud?
|
||||
return if @portal.account.feature_enabled?('help_center')
|
||||
|
||||
render 'public/api/v1/portals/not_active', status: :payment_required
|
||||
end
|
||||
end
|
||||
|
||||
@@ -98,7 +98,9 @@ export default {
|
||||
mql.onchange = e => setColorTheme(e.matches);
|
||||
},
|
||||
setLocale(locale) {
|
||||
this.$root.$i18n.locale = locale;
|
||||
if (locale) {
|
||||
this.$root.$i18n.locale = locale;
|
||||
}
|
||||
},
|
||||
async initializeAccount() {
|
||||
await this.$store.dispatch('accounts/get');
|
||||
|
||||
@@ -15,12 +15,6 @@ class ApiClient {
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
get accountIdFromRoute() {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
if (window.__WOOT_ACCOUNT_ID__) {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
return window.__WOOT_ACCOUNT_ID__;
|
||||
}
|
||||
|
||||
const isInsideAccountScopedURLs =
|
||||
window.location.pathname.includes('/app/accounts');
|
||||
|
||||
|
||||
@@ -14,8 +14,7 @@ class CacheEnabledApiClient extends ApiClient {
|
||||
}
|
||||
|
||||
get(cache = false) {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
if (cache && !window.__WOOT_ISOLATED_SHELL__) {
|
||||
if (cache) {
|
||||
return this.getFromCache();
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,10 @@ class AgentBotsAPI extends ApiClient {
|
||||
resetAccessToken(botId) {
|
||||
return axios.post(`${this.url}/${botId}/reset_access_token`);
|
||||
}
|
||||
|
||||
resetSecret(botId) {
|
||||
return axios.post(`${this.url}/${botId}/reset_secret`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new AgentBotsAPI();
|
||||
|
||||
@@ -8,14 +8,7 @@ import {
|
||||
} from '../store/utils/api';
|
||||
|
||||
export default {
|
||||
async validityCheck() {
|
||||
if (this.hasAuthToken()) {
|
||||
const urlData = endPoints('profileUpdate');
|
||||
const response = await axios.get(urlData.url);
|
||||
// to match the response signature of the validityCheck endpoint
|
||||
return Promise.resolve({ data: { payload: response } });
|
||||
}
|
||||
|
||||
validityCheck() {
|
||||
const urlData = endPoints('validityCheck');
|
||||
return axios.get(urlData.url);
|
||||
},
|
||||
@@ -38,10 +31,6 @@ export default {
|
||||
hasAuthCookie() {
|
||||
return !!Cookies.get('cw_d_session_info');
|
||||
},
|
||||
hasAuthToken() {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
return !!window.__WOOT_ACCESS_TOKEN__;
|
||||
},
|
||||
getAuthData() {
|
||||
if (this.hasAuthCookie()) {
|
||||
const savedAuthInfo = Cookies.get('cw_d_session_info');
|
||||
|
||||
@@ -31,6 +31,12 @@ class CaptainCustomTools extends ApiClient {
|
||||
delete(id) {
|
||||
return axios.delete(`${this.url}/${id}`);
|
||||
}
|
||||
|
||||
test(data = {}) {
|
||||
return axios.post(`${this.url}/test`, {
|
||||
custom_tool: data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new CaptainCustomTools();
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
/* global axios */
|
||||
import CacheEnabledApiClient from './CacheEnabledApiClient';
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const BaseClass = window.__WOOT_ISOLATED_SHELL__
|
||||
? ApiClient
|
||||
: CacheEnabledApiClient;
|
||||
|
||||
class Inboxes extends BaseClass {
|
||||
class Inboxes extends CacheEnabledApiClient {
|
||||
constructor() {
|
||||
super('inboxes', { accountScoped: true });
|
||||
}
|
||||
@@ -54,6 +48,10 @@ class Inboxes extends BaseClass {
|
||||
template,
|
||||
});
|
||||
}
|
||||
|
||||
resetSecret(inboxId) {
|
||||
return axios.post(`${this.url}/${inboxId}/reset_secret`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new Inboxes();
|
||||
|
||||
@@ -106,6 +106,10 @@ select {
|
||||
&[disabled] {
|
||||
@apply field-disabled;
|
||||
}
|
||||
|
||||
option:not(:disabled) {
|
||||
@apply bg-n-solid-2 text-n-slate-12;
|
||||
}
|
||||
}
|
||||
|
||||
// Textarea
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
// scss-lint:disable PropertySortOrder
|
||||
|
||||
// NEXT COLORS START
|
||||
@layer base {
|
||||
:root,
|
||||
:host {
|
||||
// NEXT COLORS START
|
||||
:root {
|
||||
// slate
|
||||
--slate-1: 252 252 253;
|
||||
--slate-2: 249 249 251;
|
||||
@@ -140,48 +138,6 @@
|
||||
--label-background: 247 247 247;
|
||||
--label-border: 0, 0, 0, 0.04;
|
||||
|
||||
// Bubble-specific colors (override these for custom theming)
|
||||
--bubble-agent-bg: var(--solid-blue);
|
||||
--bubble-agent-text: var(--slate-12);
|
||||
--bubble-user-bg: var(--slate-4);
|
||||
--bubble-user-text: var(--slate-12);
|
||||
--bubble-private-bg: var(--solid-amber);
|
||||
--bubble-private-text: var(--amber-12);
|
||||
--bubble-bot-bg: var(--solid-iris);
|
||||
--bubble-bot-text: var(--slate-12);
|
||||
|
||||
// Bubble border customization
|
||||
--bubble-border-width: 0;
|
||||
--bubble-agent-border: var(--blue-6);
|
||||
--bubble-user-border: var(--slate-6);
|
||||
--bubble-private-border: var(--amber-6);
|
||||
--bubble-bot-border: var(--iris-6);
|
||||
|
||||
// Bubble spacing (ratio of 1 = default, 1.5 = 50% larger, 0.5 = 50% smaller)
|
||||
--bubble-spacing-ratio: 1;
|
||||
--bubble-radius: calc(0.75rem * var(--bubble-spacing-ratio));
|
||||
--bubble-radius-sm: calc(0.125rem * var(--bubble-spacing-ratio));
|
||||
--bubble-radius-md: calc(0.375rem * var(--bubble-spacing-ratio));
|
||||
--bubble-radius-lg: calc(0.5rem * var(--bubble-spacing-ratio));
|
||||
--bubble-padding-x: calc(1rem * var(--bubble-spacing-ratio));
|
||||
--bubble-padding-y: calc(0.75rem * var(--bubble-spacing-ratio));
|
||||
|
||||
// Bubble meta text colors (per variant)
|
||||
--bubble-agent-meta: var(--slate-11);
|
||||
--bubble-user-meta: var(--slate-11);
|
||||
--bubble-private-meta: var(--amber-12);
|
||||
--bubble-bot-meta: var(--slate-11);
|
||||
|
||||
// Message status colors (per variant)
|
||||
--bubble-agent-status: var(--slate-10);
|
||||
--bubble-agent-status-read: 126 182 255;
|
||||
--bubble-user-status: var(--slate-10);
|
||||
--bubble-user-status-read: 126 182 255;
|
||||
--bubble-private-status: var(--amber-11);
|
||||
--bubble-private-status-read: 126 182 255;
|
||||
--bubble-bot-status: var(--slate-10);
|
||||
--bubble-bot-status-read: 126 182 255;
|
||||
|
||||
--alpha-1: 215, 215, 215, 0.22;
|
||||
--alpha-2: 196, 197, 198, 0.22;
|
||||
--alpha-3: 255, 255, 255, 0.96;
|
||||
@@ -191,8 +147,7 @@
|
||||
--white-alpha: 255, 255, 255, 0.8;
|
||||
}
|
||||
|
||||
.dark,
|
||||
:host(.dark) {
|
||||
.dark {
|
||||
// slate
|
||||
--slate-1: 17 17 19;
|
||||
--slate-2: 24 25 27;
|
||||
@@ -328,48 +283,6 @@
|
||||
--label-background: 36 38 45;
|
||||
--label-border: 255, 255, 255, 0.03;
|
||||
|
||||
// Bubble-specific colors (override these for custom theming)
|
||||
--bubble-agent-bg: var(--solid-blue);
|
||||
--bubble-agent-text: var(--slate-12);
|
||||
--bubble-user-bg: var(--slate-4);
|
||||
--bubble-user-text: var(--slate-12);
|
||||
--bubble-private-bg: var(--solid-amber);
|
||||
--bubble-private-text: var(--amber-12);
|
||||
--bubble-bot-bg: var(--solid-iris);
|
||||
--bubble-bot-text: var(--slate-12);
|
||||
|
||||
// Bubble border customization
|
||||
--bubble-border-width: 0;
|
||||
--bubble-agent-border: var(--blue-6);
|
||||
--bubble-user-border: var(--slate-6);
|
||||
--bubble-private-border: var(--amber-6);
|
||||
--bubble-bot-border: var(--iris-6);
|
||||
|
||||
// Bubble spacing (inherited from light mode, can be overridden)
|
||||
--bubble-spacing-ratio: 1;
|
||||
--bubble-radius: calc(0.75rem * var(--bubble-spacing-ratio));
|
||||
--bubble-radius-sm: calc(0.125rem * var(--bubble-spacing-ratio));
|
||||
--bubble-radius-md: calc(0.375rem * var(--bubble-spacing-ratio));
|
||||
--bubble-radius-lg: calc(0.5rem * var(--bubble-spacing-ratio));
|
||||
--bubble-padding-x: calc(1rem * var(--bubble-spacing-ratio));
|
||||
--bubble-padding-y: calc(0.75rem * var(--bubble-spacing-ratio));
|
||||
|
||||
// Bubble meta text colors (per variant)
|
||||
--bubble-agent-meta: var(--slate-11);
|
||||
--bubble-user-meta: var(--slate-11);
|
||||
--bubble-private-meta: var(--amber-12);
|
||||
--bubble-bot-meta: var(--slate-11);
|
||||
|
||||
// Message status colors (per variant)
|
||||
--bubble-agent-status: var(--slate-10);
|
||||
--bubble-agent-status-read: 126 182 255;
|
||||
--bubble-user-status: var(--slate-10);
|
||||
--bubble-user-status-read: 126 182 255;
|
||||
--bubble-private-status: var(--amber-11);
|
||||
--bubble-private-status-read: 126 182 255;
|
||||
--bubble-bot-status: var(--slate-10);
|
||||
--bubble-bot-status-read: 126 182 255;
|
||||
|
||||
--alpha-1: 35, 36, 42, 0.8;
|
||||
--alpha-2: 147, 153, 176, 0.12;
|
||||
--alpha-3: 33, 34, 38, 0.95;
|
||||
@@ -380,3 +293,4 @@
|
||||
--white-alpha: 255, 255, 255, 0.1;
|
||||
}
|
||||
}
|
||||
// NEXT COLORS END
|
||||
|
||||
+3
-3
@@ -20,11 +20,11 @@ const excludedLabels = defineModel('excludedLabels', {
|
||||
|
||||
const excludeOlderThanMinutes = defineModel('excludeOlderThanMinutes', {
|
||||
type: Number,
|
||||
default: 10,
|
||||
default: null,
|
||||
});
|
||||
|
||||
// Duration limits: 10 minutes to 999 days (in minutes)
|
||||
const MIN_DURATION_MINUTES = 10;
|
||||
// Duration limits: 1 minute to 999 days (in minutes)
|
||||
const MIN_DURATION_MINUTES = 1;
|
||||
const MAX_DURATION_MINUTES = 1438560; // 999 days * 24 hours * 60 minutes
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
+2
-1
@@ -27,7 +27,7 @@ const { t } = useI18n();
|
||||
|
||||
const BASE_KEY = 'ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY';
|
||||
const DEFAULT_CONVERSATION_LIMIT = 10;
|
||||
const MIN_CONVERSATION_LIMIT = 1;
|
||||
const MIN_CONVERSATION_LIMIT = 0;
|
||||
const MAX_CONVERSATION_LIMIT = 100000;
|
||||
|
||||
const selectedInboxIds = computed(
|
||||
@@ -42,6 +42,7 @@ const availableInboxes = computed(() =>
|
||||
|
||||
const isLimitValid = limit => {
|
||||
return (
|
||||
Number.isInteger(limit.conversationLimit) &&
|
||||
limit.conversationLimit >= MIN_CONVERSATION_LIMIT &&
|
||||
limit.conversationLimit <= MAX_CONVERSATION_LIMIT
|
||||
);
|
||||
|
||||
+51
-195
@@ -1,207 +1,63 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { CONVERSATION_PRIORITY } from 'shared/constants/messages';
|
||||
|
||||
defineProps({
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
|
||||
const props = defineProps({
|
||||
priority: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
showEmpty: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
const { t } = useI18n();
|
||||
|
||||
const icons = {
|
||||
[CONVERSATION_PRIORITY.URGENT]: 'i-woot-priority-urgent',
|
||||
[CONVERSATION_PRIORITY.HIGH]: 'i-woot-priority-high',
|
||||
[CONVERSATION_PRIORITY.MEDIUM]: 'i-woot-priority-medium',
|
||||
[CONVERSATION_PRIORITY.LOW]: 'i-woot-priority-low',
|
||||
};
|
||||
|
||||
const priorityLabels = {
|
||||
[CONVERSATION_PRIORITY.URGENT]: 'CONVERSATION.PRIORITY.OPTIONS.URGENT',
|
||||
[CONVERSATION_PRIORITY.HIGH]: 'CONVERSATION.PRIORITY.OPTIONS.HIGH',
|
||||
[CONVERSATION_PRIORITY.MEDIUM]: 'CONVERSATION.PRIORITY.OPTIONS.MEDIUM',
|
||||
[CONVERSATION_PRIORITY.LOW]: 'CONVERSATION.PRIORITY.OPTIONS.LOW',
|
||||
};
|
||||
|
||||
const iconName = computed(() => {
|
||||
if (props.priority && icons[props.priority]) {
|
||||
return icons[props.priority];
|
||||
}
|
||||
return props.showEmpty ? 'i-woot-priority-empty' : '';
|
||||
});
|
||||
|
||||
const tooltipContent = computed(() => {
|
||||
if (props.priority && priorityLabels[props.priority]) {
|
||||
return t(priorityLabels[props.priority]);
|
||||
}
|
||||
|
||||
if (props.showEmpty) {
|
||||
return t('CONVERSATION.PRIORITY.OPTIONS.NONE');
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable vue/no-static-inline-styles -->
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center rounded-md">
|
||||
<!-- Low Priority -->
|
||||
<svg
|
||||
v-if="priority === CONVERSATION_PRIORITY.LOW"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<mask
|
||||
id="mask0_2030_12879"
|
||||
style="mask-type: alpha"
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="0"
|
||||
y="0"
|
||||
width="20"
|
||||
height="20"
|
||||
>
|
||||
<rect width="20" height="20" fill="#D9D9D9" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_2030_12879)">
|
||||
<rect
|
||||
x="3.33301"
|
||||
y="10"
|
||||
width="3.33333"
|
||||
height="6.66667"
|
||||
rx="1.66667"
|
||||
class="fill-n-amber-9"
|
||||
/>
|
||||
<rect
|
||||
x="8.33301"
|
||||
y="6.6665"
|
||||
width="3.33333"
|
||||
height="10"
|
||||
rx="1.66667"
|
||||
class="fill-n-slate-6"
|
||||
/>
|
||||
<rect
|
||||
x="13.333"
|
||||
y="3.3335"
|
||||
width="3.33333"
|
||||
height="13.3333"
|
||||
rx="1.66667"
|
||||
class="fill-n-slate-6"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<!-- Medium Priority -->
|
||||
<svg
|
||||
v-if="priority === CONVERSATION_PRIORITY.MEDIUM"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<mask
|
||||
id="mask0_2030_12879"
|
||||
style="mask-type: alpha"
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="0"
|
||||
y="0"
|
||||
width="20"
|
||||
height="20"
|
||||
>
|
||||
<rect width="20" height="20" fill="#D9D9D9" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_2030_12879)">
|
||||
<rect
|
||||
x="3.33301"
|
||||
y="10"
|
||||
width="3.33333"
|
||||
height="6.66667"
|
||||
rx="1.66667"
|
||||
class="fill-n-amber-9"
|
||||
/>
|
||||
<rect
|
||||
x="8.33301"
|
||||
y="6.6665"
|
||||
width="3.33333"
|
||||
height="10"
|
||||
rx="1.66667"
|
||||
class="fill-n-amber-9"
|
||||
/>
|
||||
<rect
|
||||
x="13.333"
|
||||
y="3.3335"
|
||||
width="3.33333"
|
||||
height="13.3333"
|
||||
rx="1.66667"
|
||||
class="fill-n-slate-6"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<!-- High Priority -->
|
||||
<svg
|
||||
v-if="priority === CONVERSATION_PRIORITY.HIGH"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<mask
|
||||
id="mask0_2030_12879"
|
||||
style="mask-type: alpha"
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="0"
|
||||
y="0"
|
||||
width="20"
|
||||
height="20"
|
||||
>
|
||||
<rect width="20" height="20" fill="#D9D9D9" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_2030_12879)">
|
||||
<rect
|
||||
x="3.33301"
|
||||
y="10"
|
||||
width="3.33333"
|
||||
height="6.66667"
|
||||
rx="1.66667"
|
||||
class="fill-n-amber-9"
|
||||
/>
|
||||
<rect
|
||||
x="8.33301"
|
||||
y="6.6665"
|
||||
width="3.33333"
|
||||
height="10"
|
||||
rx="1.66667"
|
||||
class="fill-n-amber-9"
|
||||
/>
|
||||
<rect
|
||||
x="13.333"
|
||||
y="3.3335"
|
||||
width="3.33333"
|
||||
height="13.3333"
|
||||
rx="1.66667"
|
||||
class="fill-n-amber-9"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<!-- Urgent Priority -->
|
||||
<svg
|
||||
v-if="priority === CONVERSATION_PRIORITY.URGENT"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<mask
|
||||
id="mask0_2030_12879"
|
||||
style="mask-type: alpha"
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="0"
|
||||
y="0"
|
||||
width="20"
|
||||
height="20"
|
||||
>
|
||||
<rect width="20" height="20" fill="#D9D9D9" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_2030_12879)">
|
||||
<rect
|
||||
x="3.33301"
|
||||
y="10"
|
||||
width="3.33333"
|
||||
height="6.66667"
|
||||
rx="1.66667"
|
||||
class="fill-n-ruby-9"
|
||||
/>
|
||||
<rect
|
||||
x="8.33301"
|
||||
y="6.6665"
|
||||
width="3.33333"
|
||||
height="10"
|
||||
rx="1.66667"
|
||||
class="fill-n-ruby-9"
|
||||
/>
|
||||
<rect
|
||||
x="13.333"
|
||||
y="3.3335"
|
||||
width="3.33333"
|
||||
height="13.3333"
|
||||
rx="1.66667"
|
||||
class="fill-n-ruby-9"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<Icon
|
||||
v-tooltip.top="{
|
||||
content: tooltipContent,
|
||||
delay: { show: 500, hide: 0 },
|
||||
}"
|
||||
:icon="iconName"
|
||||
class="size-4 text-n-slate-5"
|
||||
/>
|
||||
</template>
|
||||
|
||||
+17
-10
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { debounce } from '@chatwoot/utils';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ARTICLE_EDITOR_MENU_OPTIONS } from 'dashboard/constants/editor';
|
||||
@@ -32,6 +32,7 @@ const emit = defineEmits([
|
||||
'setAuthor',
|
||||
'setCategory',
|
||||
'previewArticle',
|
||||
'createArticle',
|
||||
]);
|
||||
|
||||
const { t } = useI18n();
|
||||
@@ -57,16 +58,10 @@ const quickSave = debounce(
|
||||
// Only use to save for existing articles
|
||||
const saveAndSyncDebounced = debounce(saveAndSync, 2500, false);
|
||||
|
||||
// Debounced save for new articles
|
||||
const quickSaveNewArticle = debounce(saveAndSync, 400, false);
|
||||
|
||||
const handleSave = value => {
|
||||
if (isNewArticle.value) {
|
||||
quickSaveNewArticle(value);
|
||||
} else {
|
||||
quickSave(value);
|
||||
saveAndSyncDebounced(value);
|
||||
}
|
||||
if (isNewArticle.value) return;
|
||||
quickSave(value);
|
||||
saveAndSyncDebounced(value);
|
||||
};
|
||||
|
||||
const articleTitle = computed({
|
||||
@@ -76,9 +71,12 @@ const articleTitle = computed({
|
||||
},
|
||||
});
|
||||
|
||||
const localContent = ref(props.article.content || '');
|
||||
|
||||
const articleContent = computed({
|
||||
get: () => props.article.content,
|
||||
set: content => {
|
||||
localContent.value = content;
|
||||
handleSave({ content });
|
||||
},
|
||||
});
|
||||
@@ -98,6 +96,14 @@ const setCategoryId = categoryId => {
|
||||
const previewArticle = () => {
|
||||
emit('previewArticle');
|
||||
};
|
||||
|
||||
const handleCreateArticle = event => {
|
||||
if (!isNewArticle.value) return;
|
||||
const title = event?.target?.value || '';
|
||||
if (title.trim()) {
|
||||
emit('createArticle', { title, content: localContent.value });
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -122,6 +128,7 @@ const previewArticle = () => {
|
||||
custom-text-area-wrapper-class="border-0 !bg-transparent dark:!bg-transparent !py-0 !px-0"
|
||||
placeholder="Title"
|
||||
autofocus
|
||||
@blur="handleCreateArticle"
|
||||
/>
|
||||
<ArticleEditorControls
|
||||
:article="article"
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import CardLayout from 'dashboard/components-next/CardLayout.vue';
|
||||
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue';
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
@@ -34,14 +35,34 @@ const props = defineProps({
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
isSelected: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
selectable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showSelectionControl: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showMenu: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['action']);
|
||||
const emit = defineEmits(['action', 'select', 'hover']);
|
||||
const { checkPermissions } = usePolicy();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const [showActionsDropdown, toggleDropdown] = useToggle();
|
||||
const modelValue = computed({
|
||||
get: () => props.isSelected,
|
||||
set: () => emit('select', props.id),
|
||||
});
|
||||
|
||||
const menuItems = computed(() => {
|
||||
const allOptions = [
|
||||
@@ -79,12 +100,23 @@ const handleAction = ({ action, value }) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CardLayout>
|
||||
<CardLayout
|
||||
:selectable="selectable"
|
||||
class="relative"
|
||||
@mouseenter="emit('hover', true)"
|
||||
@mouseleave="emit('hover', false)"
|
||||
>
|
||||
<div
|
||||
v-show="showSelectionControl"
|
||||
class="absolute top-7 ltr:left-3 rtl:right-3"
|
||||
>
|
||||
<Checkbox v-model="modelValue" />
|
||||
</div>
|
||||
<div class="flex gap-1 justify-between w-full">
|
||||
<span class="text-base text-n-slate-12 line-clamp-1">
|
||||
{{ name }}
|
||||
</span>
|
||||
<div class="flex gap-2 items-center">
|
||||
<div v-if="showMenu" class="flex gap-2 items-center">
|
||||
<div
|
||||
v-on-clickaway="() => toggleDropdown(false)"
|
||||
class="flex relative items-center group"
|
||||
|
||||
+11
-5
@@ -21,16 +21,22 @@ const emit = defineEmits(['deleteSuccess']);
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
const bulkDeleteDialogRef = ref(null);
|
||||
const i18nKey = computed(() => props.type.toUpperCase());
|
||||
const i18nKey = computed(() => {
|
||||
const i18nTypeMap = {
|
||||
AssistantResponse: 'RESPONSES',
|
||||
AssistantDocument: 'DOCUMENTS',
|
||||
};
|
||||
return i18nTypeMap[props.type];
|
||||
});
|
||||
|
||||
const handleBulkDelete = async ids => {
|
||||
if (!ids) return;
|
||||
|
||||
try {
|
||||
await store.dispatch(
|
||||
'captainBulkActions/handleBulkDelete',
|
||||
Array.from(props.bulkIds)
|
||||
);
|
||||
await store.dispatch('captainBulkActions/handleBulkDelete', {
|
||||
ids: Array.from(props.bulkIds),
|
||||
type: props.type,
|
||||
});
|
||||
|
||||
emit('deleteSuccess');
|
||||
useAlert(t(`CAPTAIN.${i18nKey.value}.BULK_DELETE.SUCCESS_MESSAGE`));
|
||||
|
||||
@@ -6,6 +6,13 @@ import { useAccount } from 'dashboard/composables/useAccount';
|
||||
|
||||
import BasePaywallModal from 'dashboard/routes/dashboard/settings/components/BasePaywallModal.vue';
|
||||
|
||||
defineProps({
|
||||
featurePrefix: {
|
||||
type: String,
|
||||
default: 'CAPTAIN',
|
||||
},
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const currentUser = useMapGetter('getCurrentUser');
|
||||
|
||||
@@ -31,7 +38,7 @@ const openBilling = () => {
|
||||
>
|
||||
<BasePaywallModal
|
||||
class="mx-auto"
|
||||
feature-prefix="CAPTAIN"
|
||||
:feature-prefix="featurePrefix"
|
||||
:i18n-key="i18nKey"
|
||||
:is-super-admin="isSuperAdmin"
|
||||
:is-on-chatwoot-cloud="isOnChatwootCloud"
|
||||
|
||||
+3
-6
@@ -101,12 +101,9 @@ const authTypeLabel = computed(() => {
|
||||
</Policy>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between w-full gap-4">
|
||||
<div class="flex items-center gap-3 flex-1">
|
||||
<span
|
||||
v-if="description"
|
||||
class="text-sm truncate text-n-slate-11 flex-1"
|
||||
>
|
||||
<div class="flex items-center justify-between w-full gap-4 min-w-0">
|
||||
<div class="flex items-center gap-3 flex-1 min-w-0">
|
||||
<span v-if="description" class="text-sm truncate text-n-slate-11">
|
||||
{{ description }}
|
||||
</span>
|
||||
<span
|
||||
|
||||
+80
-6
@@ -1,9 +1,10 @@
|
||||
<script setup>
|
||||
import { reactive, computed, useTemplateRef, watch } from 'vue';
|
||||
import { reactive, computed, ref, useTemplateRef, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required } from '@vuelidate/validators';
|
||||
import { required, maxLength } from '@vuelidate/validators';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import CustomToolsAPI from 'dashboard/api/captain/customTools';
|
||||
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import TextArea from 'dashboard/components-next/textarea/TextArea.vue';
|
||||
@@ -72,8 +73,12 @@ const DEFAULT_PARAM = {
|
||||
required: false,
|
||||
};
|
||||
|
||||
// OpenAI enforces a 64-char limit on function names. The backend slug is
|
||||
// "custom_" (7 chars) + parameterized title, so cap the title conservatively.
|
||||
const MAX_TOOL_NAME_LENGTH = 55;
|
||||
|
||||
const validationRules = {
|
||||
title: { required },
|
||||
title: { required, maxLength: maxLength(MAX_TOOL_NAME_LENGTH) },
|
||||
endpoint_url: { required },
|
||||
http_method: { required },
|
||||
auth_type: { required },
|
||||
@@ -103,9 +108,15 @@ const isLoading = computed(() =>
|
||||
);
|
||||
|
||||
const getErrorMessage = (field, errorKey) => {
|
||||
return v$.value[field].$error
|
||||
? t(`CAPTAIN.CUSTOM_TOOLS.FORM.${errorKey}.ERROR`)
|
||||
: '';
|
||||
if (!v$.value[field].$error) return '';
|
||||
|
||||
const failedRule = v$.value[field].$errors[0]?.$validator;
|
||||
if (failedRule === 'maxLength') {
|
||||
return t(`CAPTAIN.CUSTOM_TOOLS.FORM.${errorKey}.MAX_LENGTH_ERROR`, {
|
||||
max: MAX_TOOL_NAME_LENGTH,
|
||||
});
|
||||
}
|
||||
return t(`CAPTAIN.CUSTOM_TOOLS.FORM.${errorKey}.ERROR`);
|
||||
};
|
||||
|
||||
const formErrors = computed(() => ({
|
||||
@@ -140,6 +151,30 @@ const handleSubmit = async () => {
|
||||
|
||||
emit('submit', state);
|
||||
};
|
||||
|
||||
const isTesting = ref(false);
|
||||
const testResult = ref(null);
|
||||
const isTestDisabled = computed(
|
||||
() => state.endpoint_url.includes('{{') || !!state.request_template
|
||||
);
|
||||
|
||||
const handleTest = async () => {
|
||||
if (!state.endpoint_url) return;
|
||||
|
||||
isTesting.value = true;
|
||||
testResult.value = null;
|
||||
try {
|
||||
const { data } = await CustomToolsAPI.test(state);
|
||||
const isOk = data.status >= 200 && data.status < 300;
|
||||
testResult.value = { success: isOk, status: data.status };
|
||||
} catch (e) {
|
||||
const message =
|
||||
e.response?.data?.error || t('CAPTAIN.CUSTOM_TOOLS.TEST.ERROR');
|
||||
testResult.value = { success: false, message };
|
||||
} finally {
|
||||
isTesting.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -248,6 +283,45 @@ const handleSubmit = async () => {
|
||||
class="[&_textarea]:font-mono"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="faded"
|
||||
color="slate"
|
||||
icon="i-lucide-play"
|
||||
:label="t('CAPTAIN.CUSTOM_TOOLS.TEST.BUTTON')"
|
||||
:is-loading="isTesting"
|
||||
:disabled="isTesting || !state.endpoint_url || isTestDisabled"
|
||||
@click="handleTest"
|
||||
/>
|
||||
<p v-if="isTestDisabled" class="text-xs text-n-slate-11">
|
||||
{{ t('CAPTAIN.CUSTOM_TOOLS.TEST.DISABLED_HINT') }}
|
||||
</p>
|
||||
<div
|
||||
v-if="testResult"
|
||||
class="flex items-center gap-2 px-3 py-2 text-xs rounded-lg"
|
||||
:class="
|
||||
testResult.success
|
||||
? 'bg-n-teal-2 text-n-teal-11'
|
||||
: 'bg-n-ruby-2 text-n-ruby-11'
|
||||
"
|
||||
>
|
||||
<span
|
||||
:class="
|
||||
testResult.success ? 'i-lucide-check-circle' : 'i-lucide-x-circle'
|
||||
"
|
||||
class="size-3.5 shrink-0"
|
||||
/>
|
||||
{{
|
||||
testResult.status
|
||||
? t('CAPTAIN.CUSTOM_TOOLS.TEST.SUCCESS', {
|
||||
status: testResult.status,
|
||||
})
|
||||
: testResult.message
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 justify-between items-center w-full">
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
+12
@@ -1,8 +1,11 @@
|
||||
<script setup>
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import EmptyStateLayout from 'dashboard/components-next/EmptyStateLayout.vue';
|
||||
import FeatureSpotlight from 'dashboard/components-next/feature-spotlight/FeatureSpotlight.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const emit = defineEmits(['click']);
|
||||
const { isOnChatwootCloud } = useAccount();
|
||||
|
||||
const onClick = () => {
|
||||
emit('click');
|
||||
@@ -10,6 +13,15 @@ const onClick = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FeatureSpotlight
|
||||
:title="$t('CAPTAIN.CUSTOM_TOOLS.EMPTY_STATE.FEATURE_SPOTLIGHT.TITLE')"
|
||||
:note="$t('CAPTAIN.CUSTOM_TOOLS.EMPTY_STATE.FEATURE_SPOTLIGHT.NOTE')"
|
||||
fallback-thumbnail="/assets/images/dashboard/captain/assistant-light.svg"
|
||||
fallback-thumbnail-dark="/assets/images/dashboard/captain/assistant-dark.svg"
|
||||
learn-more-url="https://chwt.app/hc/captain-tools"
|
||||
class="mb-8"
|
||||
:hide-actions="!isOnChatwootCloud"
|
||||
/>
|
||||
<EmptyStateLayout
|
||||
:title="$t('CAPTAIN.CUSTOM_TOOLS.EMPTY_STATE.TITLE')"
|
||||
:subtitle="$t('CAPTAIN.CUSTOM_TOOLS.EMPTY_STATE.SUBTITLE')"
|
||||
|
||||
@@ -36,7 +36,13 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['enterPress', 'input', 'blur', 'focus']);
|
||||
const emit = defineEmits([
|
||||
'enterPress',
|
||||
'escapePress',
|
||||
'input',
|
||||
'blur',
|
||||
'focus',
|
||||
]);
|
||||
|
||||
const modelValue = defineModel({
|
||||
type: [String, Number],
|
||||
@@ -49,6 +55,10 @@ const onEnterPress = () => {
|
||||
emit('enterPress');
|
||||
};
|
||||
|
||||
const onEscapePress = () => {
|
||||
emit('escapePress');
|
||||
};
|
||||
|
||||
const handleInput = event => {
|
||||
emit('input', event.target.value);
|
||||
modelValue.value = event.target.value;
|
||||
@@ -102,6 +112,7 @@ defineExpose({
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@keydown.enter.prevent="onEnterPress"
|
||||
@keydown.escape.prevent="onEscapePress"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -32,6 +32,7 @@ const convertToMinutes = newValue => {
|
||||
|
||||
const transformedValue = computed({
|
||||
get() {
|
||||
if (duration.value == null) return null;
|
||||
if (unit.value === DURATION_UNITS.MINUTES) return duration.value;
|
||||
if (unit.value === DURATION_UNITS.HOURS)
|
||||
return Math.floor(duration.value / 60);
|
||||
@@ -41,6 +42,10 @@ const transformedValue = computed({
|
||||
return 0;
|
||||
},
|
||||
set(newValue) {
|
||||
if (newValue == null || newValue === '') {
|
||||
duration.value = null;
|
||||
return;
|
||||
}
|
||||
let minuteValue = convertToMinutes(newValue);
|
||||
|
||||
duration.value = Math.min(Math.max(minuteValue, props.min), props.max);
|
||||
@@ -53,6 +58,7 @@ const transformedValue = computed({
|
||||
// this might create some confusion, especially when saving
|
||||
// this watcher fixes it by rounding the duration basically, to the nearest unit value
|
||||
watch(unit, () => {
|
||||
if (duration.value == null) return;
|
||||
let adjustedValue = convertToMinutes(transformedValue.value);
|
||||
duration.value = Math.min(Math.max(adjustedValue, props.min), props.max);
|
||||
});
|
||||
|
||||
@@ -134,11 +134,6 @@ const props = defineProps({
|
||||
senderId: { type: Number, default: null },
|
||||
senderType: { type: String, default: null },
|
||||
sourceId: { type: String, default: '' }, // eslint-disable-line vue/no-unused-properties
|
||||
forceAlignTo: {
|
||||
type: String,
|
||||
default: null,
|
||||
validator: value => value === null || ['left', 'right'].includes(value),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['retry']);
|
||||
@@ -231,16 +226,12 @@ const isBotOrAgentMessage = computed(() => {
|
||||
* @returns {import('vue').ComputedRef<'left'|'right'|'center'>} The computed orientation
|
||||
*/
|
||||
const orientation = computed(() => {
|
||||
if (props.messageType === MESSAGE_TYPES.ACTIVITY) return ORIENTATION.CENTER;
|
||||
|
||||
if (props.forceAlignTo) {
|
||||
return props.forceAlignTo === 'left' ? ORIENTATION.LEFT : ORIENTATION.RIGHT;
|
||||
}
|
||||
|
||||
if (isBotOrAgentMessage.value) {
|
||||
return ORIENTATION.RIGHT;
|
||||
}
|
||||
|
||||
if (props.messageType === MESSAGE_TYPES.ACTIVITY) return ORIENTATION.CENTER;
|
||||
|
||||
return ORIENTATION.LEFT;
|
||||
});
|
||||
|
||||
@@ -255,10 +246,6 @@ const flexOrientationClass = computed(() => {
|
||||
});
|
||||
|
||||
const gridClass = computed(() => {
|
||||
if (props.forceAlignTo) {
|
||||
return 'grid gap-y-2';
|
||||
}
|
||||
|
||||
const map = {
|
||||
[ORIENTATION.LEFT]: 'grid grid-cols-1fr',
|
||||
[ORIENTATION.RIGHT]: 'grid grid-cols-[1fr_24px]',
|
||||
@@ -268,14 +255,6 @@ const gridClass = computed(() => {
|
||||
});
|
||||
|
||||
const gridTemplate = computed(() => {
|
||||
if (props.forceAlignTo) {
|
||||
return `
|
||||
"avatar"
|
||||
"bubble"
|
||||
"meta"
|
||||
`;
|
||||
}
|
||||
|
||||
const map = {
|
||||
[ORIENTATION.LEFT]: `
|
||||
"bubble"
|
||||
@@ -298,19 +277,11 @@ const shouldGroupWithNext = computed(() => {
|
||||
|
||||
const shouldShowAvatar = computed(() => {
|
||||
if (props.messageType === MESSAGE_TYPES.ACTIVITY) return false;
|
||||
if (props.forceAlignTo) return true;
|
||||
if (orientation.value === ORIENTATION.LEFT) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const shouldShowSenderName = computed(() => {
|
||||
if (props.messageType === MESSAGE_TYPES.ACTIVITY) return false;
|
||||
if (!props.forceAlignTo) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const componentToRender = computed(() => {
|
||||
if (props.isEmailInbox && !props.private) {
|
||||
const emailInboxTypes = [MESSAGE_TYPES.INCOMING, MESSAGE_TYPES.OUTGOING];
|
||||
@@ -373,8 +344,6 @@ const componentToRender = computed(() => {
|
||||
});
|
||||
|
||||
const shouldShowContextMenu = computed(() => {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
if (window.__WOOT_DISABLE_CONTEXT_MENU__) return false;
|
||||
return !props.contentAttributes?.isUnsupported;
|
||||
});
|
||||
|
||||
@@ -441,12 +410,6 @@ const shouldRenderMessage = computed(() => {
|
||||
});
|
||||
|
||||
function openContextMenu(e) {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
if (window.__WOOT_DISABLE_CONTEXT_MENU__) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldSkipContextMenu =
|
||||
e.target?.classList.contains('skip-context-menu') ||
|
||||
['a', 'img'].includes(e.target?.tagName.toLowerCase());
|
||||
@@ -527,8 +490,7 @@ const avatarTooltip = computed(() => {
|
||||
});
|
||||
|
||||
const setupHighlightTimer = () => {
|
||||
// This is a defensive check, since in the ISOLATED shell mode, there might be no route.query
|
||||
if (Number(route?.query?.messageId) !== Number(props.id)) {
|
||||
if (Number(route.query.messageId) !== Number(props.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -586,23 +548,15 @@ provideMessageContext({
|
||||
<div
|
||||
v-if="!shouldGroupWithNext && shouldShowAvatar"
|
||||
v-tooltip.left-end="avatarTooltip"
|
||||
class="[grid-area:avatar] flex items-end gap-2"
|
||||
class="[grid-area:avatar] flex items-end"
|
||||
>
|
||||
<Avatar v-bind="avatarInfo" :size="24" />
|
||||
<span
|
||||
v-if="shouldShowSenderName"
|
||||
class="text-sm font-semibold text-n-slate-12 pb-0.5"
|
||||
>
|
||||
{{ avatarInfo.name }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="[grid-area:bubble] flex"
|
||||
:class="{
|
||||
'ltr:ml-8 rtl:mr-8 justify-end':
|
||||
!props.forceAlignTo && orientation === ORIENTATION.RIGHT,
|
||||
'ltr:mr-8 rtl:ml-8':
|
||||
!props.forceAlignTo && orientation === ORIENTATION.LEFT,
|
||||
'ltr:ml-8 rtl:mr-8 justify-end': orientation === ORIENTATION.RIGHT,
|
||||
'ltr:mr-8 rtl:ml-8': orientation === ORIENTATION.LEFT,
|
||||
'min-w-0': variant === MESSAGE_VARIANTS.EMAIL,
|
||||
}"
|
||||
@contextmenu="openContextMenu($event)"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import { defineProps, computed, reactive } from 'vue';
|
||||
import Message from './Message.vue';
|
||||
import shouldGroupWithNext from './groupWithNext.js';
|
||||
import { MESSAGE_TYPES } from './constants.js';
|
||||
import { useCamelCase } from 'dashboard/composables/useTransformKeys';
|
||||
import { useMapGetter } from 'dashboard/composables/store.js';
|
||||
import MessageApi from 'dashboard/api/inbox/message.js';
|
||||
@@ -90,6 +90,39 @@ const fetchReplyMessage = async (messageId, conversationId) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if a message should be grouped with the next message
|
||||
* @param {Number} index - Index of the current message
|
||||
* @param {Array} searchList - Array of messages to check
|
||||
* @returns {Boolean} - Whether the message should be grouped with next
|
||||
*/
|
||||
const shouldGroupWithNext = (index, searchList) => {
|
||||
if (index === searchList.length - 1) return false;
|
||||
|
||||
const current = searchList[index];
|
||||
const next = searchList[index + 1];
|
||||
|
||||
if (next.status === 'failed') return false;
|
||||
|
||||
const nextSenderId = next.senderId ?? next.sender?.id;
|
||||
const currentSenderId = current.senderId ?? current.sender?.id;
|
||||
const hasSameSender = nextSenderId === currentSenderId;
|
||||
|
||||
const nextMessageType = next.messageType;
|
||||
const currentMessageType = current.messageType;
|
||||
|
||||
const areBothTemplates =
|
||||
nextMessageType === MESSAGE_TYPES.TEMPLATE &&
|
||||
currentMessageType === MESSAGE_TYPES.TEMPLATE;
|
||||
|
||||
if (!hasSameSender || areBothTemplates) return false;
|
||||
|
||||
if (currentMessageType !== nextMessageType) return false;
|
||||
|
||||
// Check if messages are in the same minute by rounding down to nearest minute
|
||||
return Math.floor(next.createdAt / 60) === Math.floor(current.createdAt / 60);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the message that was replied to
|
||||
* @param {Object} parentMessage - The message containing the reply reference
|
||||
|
||||
@@ -9,21 +9,6 @@ import { useMessageContext } from './provider.js';
|
||||
|
||||
import { MESSAGE_STATUS, MESSAGE_TYPES } from './constants';
|
||||
|
||||
const {
|
||||
status,
|
||||
isPrivate,
|
||||
createdAt,
|
||||
sourceId,
|
||||
messageType,
|
||||
contentAttributes,
|
||||
inboxId,
|
||||
} = useMessageContext();
|
||||
|
||||
// Resolve channel type from the message's own inbox so that the status
|
||||
// indicator does not depend on `getSelectedChat` being populated. In the
|
||||
// Agent SDK shell `setActiveChat` runs from a watcher in MessageList and can
|
||||
// race with the first render of MessageMeta, leaving `currentChat` empty and
|
||||
// every channel check returning false (the spinner stays forever).
|
||||
const {
|
||||
isAFacebookInbox,
|
||||
isALineChannel,
|
||||
@@ -36,7 +21,16 @@ const {
|
||||
isAnEmailChannel,
|
||||
isAnInstagramChannel,
|
||||
isATiktokChannel,
|
||||
} = useInbox(inboxId.value);
|
||||
} = useInbox();
|
||||
|
||||
const {
|
||||
status,
|
||||
isPrivate,
|
||||
createdAt,
|
||||
sourceId,
|
||||
messageType,
|
||||
contentAttributes,
|
||||
} = useMessageContext();
|
||||
|
||||
const readableTime = computed(() =>
|
||||
messageTimestamp(createdAt.value, 'LLL d, h:mm a')
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
import { computed, ref } from 'vue';
|
||||
import { useIntervalFn } from '@vueuse/core';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { MESSAGE_STATUS, MESSAGE_VARIANTS } from './constants';
|
||||
import { useMessageContext } from './provider.js';
|
||||
import { MESSAGE_STATUS } from './constants';
|
||||
|
||||
import Icon from 'next/icon/Icon.vue';
|
||||
|
||||
@@ -16,19 +15,6 @@ const { status } = defineProps({
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
const { variant } = useMessageContext();
|
||||
|
||||
const variantPrefix = computed(() => {
|
||||
const prefixMap = {
|
||||
[MESSAGE_VARIANTS.AGENT]: 'agent',
|
||||
[MESSAGE_VARIANTS.USER]: 'user',
|
||||
[MESSAGE_VARIANTS.PRIVATE]: 'private',
|
||||
[MESSAGE_VARIANTS.BOT]: 'bot',
|
||||
[MESSAGE_VARIANTS.TEMPLATE]: 'bot',
|
||||
[MESSAGE_VARIANTS.EMAIL]: 'agent',
|
||||
};
|
||||
return prefixMap[variant.value] || 'agent';
|
||||
});
|
||||
|
||||
const progresIconSequence = [
|
||||
'i-lucide-clock-1',
|
||||
@@ -68,44 +54,14 @@ const statusIcon = computed(() => {
|
||||
return statusIconMap[status];
|
||||
});
|
||||
|
||||
const statusColorMap = {
|
||||
agent: {
|
||||
[MESSAGE_STATUS.SENT]: 'text-[rgb(var(--bubble-agent-status))]',
|
||||
[MESSAGE_STATUS.DELIVERED]: 'text-[rgb(var(--bubble-agent-status))]',
|
||||
[MESSAGE_STATUS.READ]: 'text-[rgb(var(--bubble-agent-status-read))]',
|
||||
},
|
||||
user: {
|
||||
[MESSAGE_STATUS.SENT]: 'text-[rgb(var(--bubble-user-status))]',
|
||||
[MESSAGE_STATUS.DELIVERED]: 'text-[rgb(var(--bubble-user-status))]',
|
||||
[MESSAGE_STATUS.READ]: 'text-[rgb(var(--bubble-user-status-read))]',
|
||||
},
|
||||
private: {
|
||||
[MESSAGE_STATUS.SENT]: 'text-[rgb(var(--bubble-private-status))]',
|
||||
[MESSAGE_STATUS.DELIVERED]: 'text-[rgb(var(--bubble-private-status))]',
|
||||
[MESSAGE_STATUS.READ]: 'text-[rgb(var(--bubble-private-status-read))]',
|
||||
},
|
||||
bot: {
|
||||
[MESSAGE_STATUS.SENT]: 'text-[rgb(var(--bubble-bot-status))]',
|
||||
[MESSAGE_STATUS.DELIVERED]: 'text-[rgb(var(--bubble-bot-status))]',
|
||||
[MESSAGE_STATUS.READ]: 'text-[rgb(var(--bubble-bot-status-read))]',
|
||||
},
|
||||
};
|
||||
|
||||
const statusColor = computed(() => {
|
||||
const variantMap =
|
||||
statusColorMap[variantPrefix.value] || statusColorMap.agent;
|
||||
return variantMap[status];
|
||||
});
|
||||
const statusIconMap = {
|
||||
[MESSAGE_STATUS.SENT]: 'text-n-slate-10',
|
||||
[MESSAGE_STATUS.DELIVERED]: 'text-n-slate-10',
|
||||
[MESSAGE_STATUS.READ]: 'text-[#7EB6FF]',
|
||||
};
|
||||
|
||||
const progressColorMap = {
|
||||
agent: 'text-[rgb(var(--bubble-agent-status))]',
|
||||
user: 'text-[rgb(var(--bubble-user-status))]',
|
||||
private: 'text-[rgb(var(--bubble-private-status))]',
|
||||
bot: 'text-[rgb(var(--bubble-bot-status))]',
|
||||
};
|
||||
|
||||
const progressColor = computed(() => {
|
||||
return progressColorMap[variantPrefix.value] || progressColorMap.agent;
|
||||
return statusIconMap[status];
|
||||
});
|
||||
|
||||
const tooltipText = computed(() => {
|
||||
@@ -125,7 +81,7 @@ const tooltipText = computed(() => {
|
||||
v-if="status === MESSAGE_STATUS.PROGRESS"
|
||||
v-tooltip.top-start="tooltipText"
|
||||
:icon="progessIcon"
|
||||
:class="progressColor"
|
||||
class="text-n-slate-10"
|
||||
/>
|
||||
<Icon
|
||||
v-else
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
<template>
|
||||
<svg
|
||||
class="w-8 h-4"
|
||||
viewBox="0 0 120 30"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
>
|
||||
<circle cx="15" cy="15" r="12">
|
||||
<animate
|
||||
attributeName="cy"
|
||||
from="15"
|
||||
to="5"
|
||||
begin="0s"
|
||||
dur="0.9s"
|
||||
values="15;5;15;15;15;15"
|
||||
calcMode="linear"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</circle>
|
||||
<circle cx="55" cy="15" r="12">
|
||||
<animate
|
||||
attributeName="cy"
|
||||
from="15"
|
||||
to="5"
|
||||
begin="0.1s"
|
||||
dur="0.9s"
|
||||
values="15;5;15;15;15;15"
|
||||
calcMode="linear"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</circle>
|
||||
<circle cx="95" cy="15" r="12">
|
||||
<animate
|
||||
attributeName="cy"
|
||||
from="15"
|
||||
to="5"
|
||||
begin="0.2s"
|
||||
dur="0.9s"
|
||||
values="15;5;15;15;15;15"
|
||||
calcMode="linear"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</circle>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -3,31 +3,19 @@ import { computed } from 'vue';
|
||||
import BaseBubble from './Base.vue';
|
||||
import AudioChip from 'next/message/chips/Audio.vue';
|
||||
import { useMessageContext } from '../provider.js';
|
||||
import { MESSAGE_VARIANTS } from '../constants';
|
||||
|
||||
const { attachments, variant } = useMessageContext();
|
||||
const { attachments } = useMessageContext();
|
||||
|
||||
const attachment = computed(() => {
|
||||
return attachments.value[0];
|
||||
});
|
||||
|
||||
const textClass = computed(() => {
|
||||
const textClassMap = {
|
||||
[MESSAGE_VARIANTS.AGENT]: 'text-[rgb(var(--bubble-agent-text))]',
|
||||
[MESSAGE_VARIANTS.USER]: 'text-[rgb(var(--bubble-user-text))]',
|
||||
[MESSAGE_VARIANTS.PRIVATE]: 'text-[rgb(var(--bubble-private-text))]',
|
||||
[MESSAGE_VARIANTS.BOT]: 'text-[rgb(var(--bubble-bot-text))]',
|
||||
};
|
||||
return textClassMap[variant.value] || 'text-[rgb(var(--bubble-agent-text))]';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseBubble class="bg-transparent" data-bubble-name="audio">
|
||||
<AudioChip
|
||||
:attachment="attachment"
|
||||
class="p-2 skip-context-menu"
|
||||
:class="textClass"
|
||||
class="p-2 text-n-slate-12 skip-context-menu"
|
||||
/>
|
||||
</BaseBubble>
|
||||
</template>
|
||||
|
||||
@@ -20,30 +20,25 @@ const { variant, orientation, inReplyTo, shouldGroupWithNext } =
|
||||
const { t } = useI18n();
|
||||
|
||||
const varaintBaseMap = {
|
||||
[MESSAGE_VARIANTS.AGENT]:
|
||||
'bg-[rgb(var(--bubble-agent-bg))] text-[rgb(var(--bubble-agent-text))] border-[length:var(--bubble-border-width)] border-[rgb(var(--bubble-agent-border))]',
|
||||
[MESSAGE_VARIANTS.AGENT]: 'bg-n-solid-blue text-n-slate-12',
|
||||
[MESSAGE_VARIANTS.PRIVATE]:
|
||||
'bg-[rgb(var(--bubble-private-bg))] text-[rgb(var(--bubble-private-text))] border-[length:var(--bubble-border-width)] border-[rgb(var(--bubble-private-border))] [&_.prosemirror-mention-node]:font-semibold',
|
||||
[MESSAGE_VARIANTS.USER]:
|
||||
'bg-[rgb(var(--bubble-user-bg))] text-[rgb(var(--bubble-user-text))] border-[length:var(--bubble-border-width)] border-[rgb(var(--bubble-user-border))]',
|
||||
'bg-n-solid-amber text-n-amber-12 [&_.prosemirror-mention-node]:font-semibold',
|
||||
[MESSAGE_VARIANTS.USER]: 'bg-n-slate-4 text-n-slate-12',
|
||||
[MESSAGE_VARIANTS.ACTIVITY]: 'bg-n-alpha-1 text-n-slate-11 text-sm',
|
||||
[MESSAGE_VARIANTS.BOT]:
|
||||
'bg-[rgb(var(--bubble-bot-bg))] text-[rgb(var(--bubble-bot-text))] border-[length:var(--bubble-border-width)] border-[rgb(var(--bubble-bot-border))]',
|
||||
[MESSAGE_VARIANTS.TEMPLATE]:
|
||||
'bg-[rgb(var(--bubble-bot-bg))] text-[rgb(var(--bubble-bot-text))] border-[length:var(--bubble-border-width)] border-[rgb(var(--bubble-bot-border))]',
|
||||
[MESSAGE_VARIANTS.ERROR]:
|
||||
'bg-[rgb(var(--bubble-error-bg))] text-[rgb(var(--bubble-error-text))] border-[length:var(--bubble-border-width)] border-[rgb(var(--bubble-error-border))]',
|
||||
[MESSAGE_VARIANTS.BOT]: 'bg-n-solid-iris text-n-slate-12',
|
||||
[MESSAGE_VARIANTS.TEMPLATE]: 'bg-n-solid-iris text-n-slate-12',
|
||||
[MESSAGE_VARIANTS.ERROR]: 'bg-n-ruby-4 text-n-ruby-12',
|
||||
[MESSAGE_VARIANTS.EMAIL]: 'w-full',
|
||||
[MESSAGE_VARIANTS.UNSUPPORTED]:
|
||||
'bg-[rgb(var(--bubble-unsupported-bg))]/70 border border-dashed border-[rgb(var(--bubble-unsupported-border))] text-[rgb(var(--bubble-unsupported-text))]',
|
||||
'bg-n-solid-amber/70 border border-dashed border-n-amber-12 text-n-amber-12',
|
||||
};
|
||||
|
||||
const orientationMap = {
|
||||
[ORIENTATION.LEFT]:
|
||||
'left-bubble rounded-[var(--bubble-radius)] ltr:rounded-bl-[var(--bubble-radius-sm)] rtl:rounded-br-[var(--bubble-radius-sm)]',
|
||||
'left-bubble rounded-xl ltr:rounded-bl-sm rtl:rounded-br-sm',
|
||||
[ORIENTATION.RIGHT]:
|
||||
'right-bubble rounded-[var(--bubble-radius)] ltr:rounded-br-[var(--bubble-radius-sm)] rtl:rounded-bl-[var(--bubble-radius-sm)]',
|
||||
[ORIENTATION.CENTER]: 'rounded-[var(--bubble-radius-md)]',
|
||||
'right-bubble rounded-xl ltr:rounded-br-sm rtl:rounded-bl-sm',
|
||||
[ORIENTATION.CENTER]: 'rounded-md',
|
||||
};
|
||||
|
||||
const flexOrientationClass = computed(() => {
|
||||
@@ -62,7 +57,7 @@ const messageClass = computed(() => {
|
||||
if (variant.value !== MESSAGE_VARIANTS.ACTIVITY) {
|
||||
classToApply.push(orientationMap[orientation.value]);
|
||||
} else {
|
||||
classToApply.push('rounded-[var(--bubble-radius-lg)]');
|
||||
classToApply.push('rounded-lg');
|
||||
}
|
||||
|
||||
return classToApply;
|
||||
@@ -81,20 +76,6 @@ const shouldShowMeta = computed(
|
||||
variant.value !== MESSAGE_VARIANTS.ACTIVITY
|
||||
);
|
||||
|
||||
const metaColorClass = computed(() => {
|
||||
const metaClassMap = {
|
||||
[MESSAGE_VARIANTS.AGENT]: 'text-[rgb(var(--bubble-agent-meta))]',
|
||||
[MESSAGE_VARIANTS.USER]: 'text-[rgb(var(--bubble-user-meta))]',
|
||||
[MESSAGE_VARIANTS.PRIVATE]:
|
||||
'text-[rgb(var(--bubble-private-meta))] opacity-50',
|
||||
[MESSAGE_VARIANTS.BOT]: 'text-[rgb(var(--bubble-bot-meta))]',
|
||||
[MESSAGE_VARIANTS.TEMPLATE]: 'text-[rgb(var(--bubble-bot-meta))]',
|
||||
[MESSAGE_VARIANTS.EMAIL]: 'text-[rgb(var(--bubble-agent-meta))]',
|
||||
[MESSAGE_VARIANTS.ERROR]: 'text-[rgb(var(--bubble-error-meta))]',
|
||||
};
|
||||
return metaClassMap[variant.value] || 'text-[rgb(var(--bubble-agent-meta))]';
|
||||
});
|
||||
|
||||
const replyToPreview = computed(() => {
|
||||
if (!inReplyTo) return '';
|
||||
|
||||
@@ -138,7 +119,9 @@ const replyToPreview = computed(() => {
|
||||
:class="[
|
||||
flexOrientationClass,
|
||||
variant === MESSAGE_VARIANTS.EMAIL ? 'px-3 pb-3' : '',
|
||||
metaColorClass,
|
||||
variant === MESSAGE_VARIANTS.PRIVATE
|
||||
? 'text-n-amber-12/50'
|
||||
: 'text-n-slate-11',
|
||||
]"
|
||||
class="mt-2"
|
||||
/>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useI18n } from 'vue-i18n';
|
||||
import BaseBubble from './Base.vue';
|
||||
import Icon from 'next/icon/Icon.vue';
|
||||
import { useMessageContext } from '../provider.js';
|
||||
import { MESSAGE_VARIANTS } from '../constants';
|
||||
|
||||
defineProps({
|
||||
icon: { type: [String, Object], required: true },
|
||||
@@ -21,39 +20,16 @@ defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const { sender, variant } = useMessageContext();
|
||||
const { sender } = useMessageContext();
|
||||
const { t } = useI18n();
|
||||
|
||||
const senderName = computed(() => {
|
||||
return sender?.value?.name || '';
|
||||
});
|
||||
|
||||
const primaryTextClass = computed(() => {
|
||||
const classMap = {
|
||||
[MESSAGE_VARIANTS.AGENT]: 'text-[rgb(var(--bubble-agent-text))]',
|
||||
[MESSAGE_VARIANTS.USER]: 'text-[rgb(var(--bubble-user-text))]',
|
||||
[MESSAGE_VARIANTS.PRIVATE]: 'text-[rgb(var(--bubble-private-text))]',
|
||||
[MESSAGE_VARIANTS.BOT]: 'text-[rgb(var(--bubble-bot-text))]',
|
||||
};
|
||||
return classMap[variant.value] || 'text-[rgb(var(--bubble-agent-text))]';
|
||||
});
|
||||
|
||||
const secondaryTextClass = computed(() => {
|
||||
const classMap = {
|
||||
[MESSAGE_VARIANTS.AGENT]: 'text-[rgb(var(--bubble-agent-meta))]',
|
||||
[MESSAGE_VARIANTS.USER]: 'text-[rgb(var(--bubble-user-meta))]',
|
||||
[MESSAGE_VARIANTS.PRIVATE]: 'text-[rgb(var(--bubble-private-meta))]',
|
||||
[MESSAGE_VARIANTS.BOT]: 'text-[rgb(var(--bubble-bot-meta))]',
|
||||
};
|
||||
return classMap[variant.value] || 'text-[rgb(var(--bubble-agent-meta))]';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseBubble
|
||||
class="overflow-hidden p-[var(--bubble-padding-y)]"
|
||||
data-bubble-name="attachment"
|
||||
>
|
||||
<BaseBubble class="overflow-hidden p-3" data-bubble-name="attachment">
|
||||
<div class="grid gap-4 min-w-64">
|
||||
<div class="grid gap-3">
|
||||
<div
|
||||
@@ -65,11 +41,7 @@ const secondaryTextClass = computed(() => {
|
||||
</slot>
|
||||
</div>
|
||||
<div class="space-y-1 overflow-hidden">
|
||||
<div
|
||||
v-if="senderName"
|
||||
class="text-sm truncate"
|
||||
:class="primaryTextClass"
|
||||
>
|
||||
<div v-if="senderName" class="text-n-slate-12 text-sm truncate">
|
||||
{{
|
||||
t(senderTranslationKey, {
|
||||
sender: senderName,
|
||||
@@ -77,18 +49,10 @@ const secondaryTextClass = computed(() => {
|
||||
}}
|
||||
</div>
|
||||
<slot>
|
||||
<div
|
||||
v-if="title"
|
||||
class="truncate text-sm"
|
||||
:class="primaryTextClass"
|
||||
>
|
||||
<div v-if="title" class="truncate text-sm text-n-slate-12">
|
||||
{{ title }}
|
||||
</div>
|
||||
<div
|
||||
v-if="content"
|
||||
class="truncate text-sm"
|
||||
:class="secondaryTextClass"
|
||||
>
|
||||
<div v-if="content" class="truncate text-sm text-n-slate-11">
|
||||
{{ content }}
|
||||
</div>
|
||||
</slot>
|
||||
|
||||
@@ -40,10 +40,7 @@ const starRatingValue = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseBubble
|
||||
class="px-[var(--bubble-padding-x)] py-[var(--bubble-padding-y)]"
|
||||
data-bubble-name="csat"
|
||||
>
|
||||
<BaseBubble class="px-4 py-3" data-bubble-name="csat">
|
||||
<h4>{{ content || t('CONVERSATION.CSAT_REPLY_MESSAGE') }}</h4>
|
||||
<dl v-if="isRatingSubmitted" class="mt-4">
|
||||
<dt class="text-n-slate-11 italic">
|
||||
|
||||
@@ -1,32 +1,14 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { MESSAGE_STATUS, MESSAGE_TYPES } from '../../constants';
|
||||
import { MESSAGE_STATUS } from '../../constants';
|
||||
import { useMessageContext } from '../../provider.js';
|
||||
|
||||
const { contentAttributes, status, sender, messageType } = useMessageContext();
|
||||
const { contentAttributes, status, sender } = useMessageContext();
|
||||
|
||||
const hasError = computed(() => {
|
||||
return status.value === MESSAGE_STATUS.FAILED;
|
||||
});
|
||||
|
||||
const isIncoming = computed(() => {
|
||||
return messageType.value === MESSAGE_TYPES.INCOMING;
|
||||
});
|
||||
|
||||
const secondaryTextClass = computed(() => {
|
||||
if (hasError.value) return 'text-[rgb(var(--bubble-error-meta))]';
|
||||
return isIncoming.value
|
||||
? 'text-[rgb(var(--bubble-user-meta))]'
|
||||
: 'text-[rgb(var(--bubble-agent-meta))]';
|
||||
});
|
||||
|
||||
const primaryTextClass = computed(() => {
|
||||
if (hasError.value) return 'text-[rgb(var(--bubble-error-text))]';
|
||||
return isIncoming.value
|
||||
? 'text-[rgb(var(--bubble-user-text))]'
|
||||
: 'text-[rgb(var(--bubble-agent-text))]';
|
||||
});
|
||||
|
||||
const fromEmail = computed(() => {
|
||||
return contentAttributes.value?.email?.from ?? [];
|
||||
});
|
||||
@@ -86,10 +68,13 @@ const showMeta = computed(() => {
|
||||
<section
|
||||
v-show="showMeta"
|
||||
class="space-y-1 rtl:pl-9 ltr:pr-9 text-sm break-words"
|
||||
:class="secondaryTextClass"
|
||||
:class="hasError ? 'text-n-ruby-11' : 'text-n-slate-11'"
|
||||
>
|
||||
<template v-if="showMeta">
|
||||
<div v-if="fromEmail[0]" :class="primaryTextClass">
|
||||
<div
|
||||
v-if="fromEmail[0]"
|
||||
:class="hasError ? 'text-n-ruby-11' : 'text-n-slate-12'"
|
||||
>
|
||||
<template v-if="senderName">
|
||||
<span>
|
||||
{{ senderName }}
|
||||
|
||||
@@ -104,26 +104,21 @@ const handleSeeOriginal = () => {
|
||||
|
||||
<template>
|
||||
<BaseBubble
|
||||
class="w-full border-[length:var(--bubble-border-width)]"
|
||||
class="w-full"
|
||||
:class="{
|
||||
'bg-[rgb(var(--bubble-user-bg))] text-[rgb(var(--bubble-user-text))] border-[rgb(var(--bubble-user-border))]':
|
||||
isIncoming,
|
||||
'bg-[rgb(var(--bubble-agent-bg))] text-[rgb(var(--bubble-agent-text))] border-[rgb(var(--bubble-agent-border))]':
|
||||
isOutgoing,
|
||||
'bg-n-slate-4': isIncoming,
|
||||
'bg-n-solid-blue': isOutgoing,
|
||||
}"
|
||||
data-bubble-name="email"
|
||||
>
|
||||
<EmailMeta
|
||||
class="px-[var(--bubble-padding-x)] py-[var(--bubble-padding-y)]"
|
||||
class="p-3"
|
||||
:class="{
|
||||
'border-b border-[rgb(var(--bubble-user-border))]': isIncoming,
|
||||
'border-b border-[rgb(var(--bubble-agent-border))]': isOutgoing,
|
||||
'border-b border-n-strong': isIncoming,
|
||||
'border-b border-n-slate-8/20': isOutgoing,
|
||||
}"
|
||||
/>
|
||||
<section
|
||||
ref="contentContainer"
|
||||
class="px-[var(--bubble-padding-x)] py-[var(--bubble-padding-y)]"
|
||||
>
|
||||
<section ref="contentContainer" class="p-3">
|
||||
<div
|
||||
:class="{
|
||||
'max-h-[400px] overflow-hidden relative': !isExpanded && isExpandable,
|
||||
@@ -134,18 +129,14 @@ const handleSeeOriginal = () => {
|
||||
v-if="isExpandable && !isExpanded"
|
||||
class="absolute left-0 right-0 bottom-0 h-40 px-8 flex items-end"
|
||||
:class="{
|
||||
'bg-gradient-to-t from-[rgb(var(--bubble-user-bg))] via-[rgb(var(--bubble-user-bg))] via-20% to-transparent':
|
||||
'bg-gradient-to-t from-n-slate-4 via-n-slate-4 via-20% to-transparent':
|
||||
isIncoming,
|
||||
'bg-gradient-to-t from-[rgb(var(--bubble-agent-bg))] via-[rgb(var(--bubble-agent-bg))] via-20% to-transparent':
|
||||
'bg-gradient-to-t from-n-solid-blue via-n-solid-blue via-20% to-transparent':
|
||||
isOutgoing,
|
||||
}"
|
||||
>
|
||||
<button
|
||||
class="py-2 px-8 mx-auto text-center flex items-center gap-2"
|
||||
:class="{
|
||||
'text-[rgb(var(--bubble-user-text))]': isIncoming,
|
||||
'text-[rgb(var(--bubble-agent-text))]': isOutgoing,
|
||||
}"
|
||||
class="text-n-slate-12 py-2 px-8 mx-auto text-center flex items-center gap-2"
|
||||
@click="isExpanded = true"
|
||||
>
|
||||
<Icon icon="i-lucide-maximize-2" />
|
||||
@@ -154,7 +145,7 @@ const handleSeeOriginal = () => {
|
||||
</div>
|
||||
<FormattedContent
|
||||
v-if="isOutgoing && content && !hasEmailContent"
|
||||
class="text-[rgb(var(--bubble-agent-text))]"
|
||||
class="text-n-slate-12"
|
||||
:content="messageContent"
|
||||
/>
|
||||
<template v-else>
|
||||
@@ -185,11 +176,7 @@ const handleSeeOriginal = () => {
|
||||
</template>
|
||||
<button
|
||||
v-if="hasQuotedMessage"
|
||||
class="px-1 leading-none text-sm bg-n-alpha-black2 text-center flex items-center gap-1 mt-2"
|
||||
:class="{
|
||||
'text-[rgb(var(--bubble-user-meta))]': isIncoming,
|
||||
'text-[rgb(var(--bubble-agent-meta))]': isOutgoing,
|
||||
}"
|
||||
class="text-n-slate-11 px-1 leading-none text-sm bg-n-alpha-black2 text-center flex items-center gap-1 mt-2"
|
||||
@click="showQuotedMessage = !showQuotedMessage"
|
||||
>
|
||||
<template v-if="showQuotedMessage">
|
||||
|
||||
@@ -48,10 +48,7 @@ const formValues = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseBubble
|
||||
class="px-[var(--bubble-padding-x)] py-[var(--bubble-padding-y)]"
|
||||
data-bubble-name="form"
|
||||
>
|
||||
<BaseBubble class="px-4 py-3" data-bubble-name="csat">
|
||||
<span v-dompurify-html="content" :title="content" />
|
||||
<dl v-if="formValues.length" class="mt-4">
|
||||
<template v-for="item in formValues" :key="item.title">
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useLoadWithRetry } from 'dashboard/composables/loadWithRetry';
|
||||
import BaseBubble from './Base.vue';
|
||||
import Button from 'next/button/Button.vue';
|
||||
import Icon from 'next/icon/Icon.vue';
|
||||
import { useGallery } from '../useGallery';
|
||||
import { useSnakeCase } from 'dashboard/composables/useTransformKeys';
|
||||
import { useMessageContext } from '../provider.js';
|
||||
import { downloadFile } from '@chatwoot/utils';
|
||||
@@ -23,8 +22,8 @@ const attachment = computed(() => {
|
||||
|
||||
const { isLoaded, hasError, loadWithRetry } = useLoadWithRetry();
|
||||
|
||||
const showGallery = ref(false);
|
||||
const isDownloading = ref(false);
|
||||
const { showGallery, isGalleryAllowed, toggleGallery } = useGallery();
|
||||
|
||||
onMounted(() => {
|
||||
if (attachment.value?.dataUrl) {
|
||||
@@ -51,16 +50,13 @@ const handleImageError = () => {
|
||||
|
||||
<template>
|
||||
<BaseBubble
|
||||
class="overflow-hidden p-[var(--bubble-padding-y)]"
|
||||
class="overflow-hidden p-3"
|
||||
data-bubble-name="image"
|
||||
@click="toggleGallery(true)"
|
||||
@click="showGallery = true"
|
||||
>
|
||||
<div
|
||||
v-if="hasError"
|
||||
class="flex items-center gap-1 text-center rounded-lg text-[rgb(var(--bubble-error-meta))]"
|
||||
>
|
||||
<Icon icon="i-lucide-circle-off" />
|
||||
<p class="mb-0">
|
||||
<div v-if="hasError" class="flex items-center gap-1 text-center rounded-lg">
|
||||
<Icon icon="i-lucide-circle-off" class="text-n-slate-11" />
|
||||
<p class="mb-0 text-n-slate-11">
|
||||
{{ $t('COMPONENTS.MEDIA.IMAGE_UNAVAILABLE') }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -90,7 +86,7 @@ const handleImageError = () => {
|
||||
</div>
|
||||
</BaseBubble>
|
||||
<GalleryView
|
||||
v-if="showGallery && isGalleryAllowed"
|
||||
v-if="showGallery"
|
||||
v-model:show="showGallery"
|
||||
:attachment="useSnakeCase(attachment)"
|
||||
:all-attachments="filteredCurrentChatAttachments"
|
||||
|
||||
@@ -44,10 +44,7 @@ const onVideoLoadError = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseBubble
|
||||
class="p-[var(--bubble-padding-y)] overflow-hidden"
|
||||
data-bubble-name="ig-story"
|
||||
>
|
||||
<BaseBubble class="p-3 overflow-hidden" data-bubble-name="ig-story">
|
||||
<p v-if="isStoryReply" class="mb-1 text-xs text-n-slate-11">
|
||||
{{ t('COMPONENTS.FILE_BUBBLE.INSTAGRAM_STORY_REPLY') }}
|
||||
</p>
|
||||
@@ -67,10 +64,10 @@ const onVideoLoadError = () => {
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="flex items-center gap-1 px-5 py-4 text-center rounded-lg bg-n-alpha-1 text-[rgb(var(--bubble-error-meta))]"
|
||||
class="flex items-center gap-1 px-5 py-4 text-center rounded-lg bg-n-alpha-1"
|
||||
>
|
||||
<Icon icon="i-lucide-circle-off" />
|
||||
<p class="mb-0">
|
||||
<Icon icon="i-lucide-circle-off" class="text-n-slate-11" />
|
||||
<p class="mb-0 text-n-slate-11">
|
||||
{{ $t('COMPONENTS.FILE_BUBBLE.INSTAGRAM_STORY_UNAVAILABLE') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -4,11 +4,11 @@ import BaseBubble from 'next/message/bubbles/Base.vue';
|
||||
import FormattedContent from './FormattedContent.vue';
|
||||
import AttachmentChips from 'next/message/chips/AttachmentChips.vue';
|
||||
import TranslationToggle from 'dashboard/components-next/message/TranslationToggle.vue';
|
||||
import { MESSAGE_TYPES, MESSAGE_VARIANTS } from '../../constants';
|
||||
import { MESSAGE_TYPES } from '../../constants';
|
||||
import { useMessageContext } from '../../provider.js';
|
||||
import { useTranslations } from 'dashboard/composables/useTranslations';
|
||||
|
||||
const { content, attachments, contentAttributes, messageType, variant } =
|
||||
const { content, attachments, contentAttributes, messageType } =
|
||||
useMessageContext();
|
||||
|
||||
const { hasTranslations, translationContent } =
|
||||
@@ -36,29 +36,15 @@ const isEmpty = computed(() => {
|
||||
return !content.value && !attachments.value?.length;
|
||||
});
|
||||
|
||||
const emptyTextClass = computed(() => {
|
||||
const metaClassMap = {
|
||||
[MESSAGE_VARIANTS.AGENT]: 'text-[rgb(var(--bubble-agent-meta))]',
|
||||
[MESSAGE_VARIANTS.USER]: 'text-[rgb(var(--bubble-user-meta))]',
|
||||
[MESSAGE_VARIANTS.PRIVATE]: 'text-[rgb(var(--bubble-private-meta))]',
|
||||
[MESSAGE_VARIANTS.BOT]: 'text-[rgb(var(--bubble-bot-meta))]',
|
||||
[MESSAGE_VARIANTS.TEMPLATE]: 'text-[rgb(var(--bubble-bot-meta))]',
|
||||
};
|
||||
return metaClassMap[variant.value] || 'text-[rgb(var(--bubble-agent-meta))]';
|
||||
});
|
||||
|
||||
const handleSeeOriginal = () => {
|
||||
renderOriginal.value = !renderOriginal.value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseBubble
|
||||
class="px-[var(--bubble-padding-x)] py-[var(--bubble-padding-y)]"
|
||||
data-bubble-name="text"
|
||||
>
|
||||
<BaseBubble class="px-4 py-3" data-bubble-name="text">
|
||||
<div class="gap-3 flex flex-col">
|
||||
<span v-if="isEmpty" :class="emptyTextClass">
|
||||
<span v-if="isEmpty" class="text-n-slate-11">
|
||||
{{ $t('CONVERSATION.NO_CONTENT') }}
|
||||
</span>
|
||||
<FormattedContent v-if="renderContent" :content="renderContent" />
|
||||
|
||||
@@ -21,10 +21,7 @@ const unsupportedMessageKey = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseBubble
|
||||
class="px-[var(--bubble-padding-x)] py-[var(--bubble-padding-y)] text-sm"
|
||||
data-bubble-name="unsupported"
|
||||
>
|
||||
<BaseBubble class="px-4 py-3 text-sm" data-bubble-name="unsupported">
|
||||
{{ $t(unsupportedMessageKey) }}
|
||||
</BaseBubble>
|
||||
</template>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { ref, computed } from 'vue';
|
||||
import BaseBubble from './Base.vue';
|
||||
import Icon from 'next/icon/Icon.vue';
|
||||
import { useGallery } from '../useGallery';
|
||||
import { useSnakeCase } from 'dashboard/composables/useTransformKeys';
|
||||
import { useMessageContext } from '../provider.js';
|
||||
import GalleryView from 'dashboard/components/widgets/conversation/components/GalleryView.vue';
|
||||
@@ -10,7 +9,7 @@ import { ATTACHMENT_TYPES } from '../constants';
|
||||
|
||||
const emit = defineEmits(['error']);
|
||||
const hasError = ref(false);
|
||||
const { showGallery, isGalleryAllowed, toggleGallery } = useGallery();
|
||||
const showGallery = ref(false);
|
||||
const { filteredCurrentChatAttachments, attachments } = useMessageContext();
|
||||
|
||||
const handleError = () => {
|
||||
@@ -29,9 +28,9 @@ const isReel = computed(() => {
|
||||
|
||||
<template>
|
||||
<BaseBubble
|
||||
class="overflow-hidden p-[var(--bubble-padding-y)]"
|
||||
class="overflow-hidden p-3"
|
||||
data-bubble-name="video"
|
||||
@click="toggleGallery(true)"
|
||||
@click="showGallery = true"
|
||||
>
|
||||
<div class="relative group rounded-lg overflow-hidden">
|
||||
<div
|
||||
@@ -54,7 +53,7 @@ const isReel = computed(() => {
|
||||
</div>
|
||||
</BaseBubble>
|
||||
<GalleryView
|
||||
v-if="showGallery && isGalleryAllowed"
|
||||
v-if="showGallery"
|
||||
v-model:show="showGallery"
|
||||
:attachment="useSnakeCase(attachment)"
|
||||
:all-attachments="filteredCurrentChatAttachments"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import Icon from 'next/icon/Icon.vue';
|
||||
import { useGallery } from '../useGallery';
|
||||
import { useSnakeCase } from 'dashboard/composables/useTransformKeys';
|
||||
import { useMessageContext } from '../provider.js';
|
||||
|
||||
@@ -14,7 +13,7 @@ defineProps({
|
||||
},
|
||||
});
|
||||
const hasError = ref(false);
|
||||
const { showGallery, toggleGallery, isGalleryAllowed } = useGallery();
|
||||
const showGallery = ref(false);
|
||||
|
||||
const { filteredCurrentChatAttachments } = useMessageContext();
|
||||
|
||||
@@ -26,7 +25,7 @@ const handleError = () => {
|
||||
<template>
|
||||
<div
|
||||
class="size-[72px] overflow-hidden contain-content rounded-xl cursor-pointer"
|
||||
@click="toggleGallery(true)"
|
||||
@click="showGallery = true"
|
||||
>
|
||||
<div
|
||||
v-if="hasError"
|
||||
@@ -43,7 +42,7 @@ const handleError = () => {
|
||||
/>
|
||||
</div>
|
||||
<GalleryView
|
||||
v-if="showGallery && isGalleryAllowed"
|
||||
v-if="showGallery"
|
||||
v-model:show="showGallery"
|
||||
:attachment="useSnakeCase(attachment)"
|
||||
:all-attachments="filteredCurrentChatAttachments"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import Icon from 'next/icon/Icon.vue';
|
||||
import { useGallery } from '../useGallery';
|
||||
import { useSnakeCase } from 'dashboard/composables/useTransformKeys';
|
||||
import { useMessageContext } from '../provider.js';
|
||||
import GalleryView from 'dashboard/components/widgets/conversation/components/GalleryView.vue';
|
||||
@@ -12,7 +12,7 @@ defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const { showGallery, isGalleryAllowed, toggleGallery } = useGallery();
|
||||
const showGallery = ref(false);
|
||||
|
||||
const { filteredCurrentChatAttachments } = useMessageContext();
|
||||
</script>
|
||||
@@ -20,7 +20,7 @@ const { filteredCurrentChatAttachments } = useMessageContext();
|
||||
<template>
|
||||
<div
|
||||
class="size-[72px] overflow-hidden contain-content rounded-xl cursor-pointer relative group"
|
||||
@click="toggleGallery(true)"
|
||||
@click="showGallery = true"
|
||||
>
|
||||
<video
|
||||
:src="attachment.dataUrl"
|
||||
@@ -42,7 +42,7 @@ const { filteredCurrentChatAttachments } = useMessageContext();
|
||||
</div>
|
||||
</div>
|
||||
<GalleryView
|
||||
v-if="showGallery && isGalleryAllowed"
|
||||
v-if="showGallery"
|
||||
v-model:show="showGallery"
|
||||
:attachment="useSnakeCase(attachment)"
|
||||
:all-attachments="filteredCurrentChatAttachments"
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import { MESSAGE_TYPES } from './constants';
|
||||
/**
|
||||
* Determines if a message should be grouped with the next message
|
||||
* @param {Number} index - Index of the current message
|
||||
* @param {Array} searchList - Array of messages to check
|
||||
* @returns {Boolean} - Whether the message should be grouped with next
|
||||
*/
|
||||
export default (index, searchList) => {
|
||||
if (index === searchList.length - 1) return false;
|
||||
|
||||
const current = searchList[index];
|
||||
const next = searchList[index + 1];
|
||||
|
||||
if (next.status === 'failed') return false;
|
||||
|
||||
const nextSenderId = next.senderId ?? next.sender?.id;
|
||||
const currentSenderId = current.senderId ?? current.sender?.id;
|
||||
const hasSameSender = nextSenderId === currentSenderId;
|
||||
|
||||
const nextMessageType = next.messageType;
|
||||
const currentMessageType = current.messageType;
|
||||
|
||||
const areBothTemplates =
|
||||
nextMessageType === MESSAGE_TYPES.TEMPLATE &&
|
||||
currentMessageType === MESSAGE_TYPES.TEMPLATE;
|
||||
|
||||
if (!hasSameSender || areBothTemplates) return false;
|
||||
|
||||
if (currentMessageType !== nextMessageType) return false;
|
||||
|
||||
// Check if messages are in the same minute by rounding down to nearest minute
|
||||
return Math.floor(next.createdAt / 60) === Math.floor(current.createdAt / 60);
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
import { computed } from 'vue';
|
||||
import { useToggle } from '@vueuse/core';
|
||||
|
||||
export function useGallery() {
|
||||
const [showGallery] = useToggle(false);
|
||||
|
||||
const isGalleryAllowed = computed(() => {
|
||||
return true;
|
||||
});
|
||||
|
||||
const toggleGallery = value => {
|
||||
showGallery.value = value;
|
||||
};
|
||||
|
||||
return {
|
||||
showGallery,
|
||||
isGalleryAllowed,
|
||||
toggleGallery,
|
||||
};
|
||||
}
|
||||
@@ -15,7 +15,6 @@ const { modalType, closeOnBackdropClick, onClose } = defineProps({
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
const show = defineModel('show', { type: Boolean, default: false });
|
||||
const teleportTarget = ref(null);
|
||||
|
||||
const modalClassName = computed(() => {
|
||||
const modalClassNameMap = {
|
||||
@@ -26,24 +25,6 @@ const modalClassName = computed(() => {
|
||||
return `modal-mask skip-context-menu ${modalClassNameMap[modalType] || ''}`;
|
||||
});
|
||||
|
||||
const resolveTeleportTarget = () => {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
if (window.__WOOT_ISOLATED_SHELL__) {
|
||||
// Look for the specific chatwoot-message-list custom element
|
||||
const messageListElement = document.querySelector('chatwoot-message-list');
|
||||
if (messageListElement?.shadowRoot) {
|
||||
const modalRoot =
|
||||
messageListElement.shadowRoot.querySelector('#cw-modal-root');
|
||||
if (modalRoot) {
|
||||
teleportTarget.value = modalRoot;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
teleportTarget.value = document.body;
|
||||
};
|
||||
|
||||
// [TODO] Revisit this logic to use outside click directive
|
||||
const mousedDownOnBackdrop = ref(false);
|
||||
|
||||
@@ -77,7 +58,6 @@ useEventListener(document.body, 'mouseup', onMouseUp);
|
||||
useEventListener(document, 'keydown', onKeydown);
|
||||
|
||||
onMounted(() => {
|
||||
resolveTeleportTarget();
|
||||
if (import.meta.env.DEV && onClose && typeof onClose === 'function') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
@@ -88,38 +68,36 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport v-if="teleportTarget" :to="teleportTarget">
|
||||
<transition name="modal-fade">
|
||||
<transition name="modal-fade">
|
||||
<div
|
||||
v-if="show"
|
||||
:class="modalClassName"
|
||||
transition="modal"
|
||||
@mousedown="handleMouseDown"
|
||||
>
|
||||
<div
|
||||
v-if="show"
|
||||
:class="modalClassName"
|
||||
transition="modal"
|
||||
@mousedown="handleMouseDown"
|
||||
class="relative max-h-full overflow-auto bg-n-alpha-3 shadow-md modal-container rtl:text-right skip-context-menu"
|
||||
:class="{
|
||||
'rounded-xl w-[37.5rem]': !fullWidth,
|
||||
'items-center rounded-none flex h-full justify-center w-full':
|
||||
fullWidth,
|
||||
[size]: true,
|
||||
}"
|
||||
@mouse.stop
|
||||
@mousedown="event => event.stopPropagation()"
|
||||
>
|
||||
<div
|
||||
class="relative max-h-full overflow-auto bg-n-alpha-3 shadow-md modal-container rtl:text-right skip-context-menu"
|
||||
:class="{
|
||||
'rounded-xl w-[37.5rem]': !fullWidth,
|
||||
'items-center rounded-none flex h-full justify-center w-full':
|
||||
fullWidth,
|
||||
[size]: true,
|
||||
}"
|
||||
@mouse.stop
|
||||
@mousedown="event => event.stopPropagation()"
|
||||
>
|
||||
<Button
|
||||
v-if="showCloseButton"
|
||||
ghost
|
||||
slate
|
||||
icon="i-lucide-x"
|
||||
class="absolute z-10 ltr:right-2 rtl:left-2 top-2"
|
||||
@click="close"
|
||||
/>
|
||||
<slot />
|
||||
</div>
|
||||
<Button
|
||||
v-if="showCloseButton"
|
||||
ghost
|
||||
slate
|
||||
icon="i-lucide-x"
|
||||
class="absolute z-10 ltr:right-2 rtl:left-2 top-2"
|
||||
@click="close"
|
||||
/>
|
||||
<slot />
|
||||
</div>
|
||||
</transition>
|
||||
</Teleport>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -89,7 +89,6 @@ const props = defineProps({
|
||||
// are triggered except when this flag is true
|
||||
allowSignature: { type: Boolean, default: false },
|
||||
channelType: { type: String, default: '' },
|
||||
forceSignature: { type: Boolean, default: false },
|
||||
conversationId: { type: Number, default: null },
|
||||
medium: { type: String, default: '' },
|
||||
showImageResizeToolbar: { type: Boolean, default: false }, // A kill switch to show or hide the image toolbar
|
||||
@@ -314,11 +313,12 @@ const plugins = computed(() => {
|
||||
const sendWithSignature = computed(() => {
|
||||
// this is considered the source of truth, we watch this property
|
||||
// on change, we toggle the signature in the editor
|
||||
if (props.forceSignature && props.allowSignature && !props.isPrivate) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (props.allowSignature && !props.isPrivate && props.channelType) {
|
||||
if (
|
||||
props.allowSignature &&
|
||||
!props.isPrivate &&
|
||||
props.channelType &&
|
||||
!props.disabled
|
||||
) {
|
||||
return fetchSignatureFlagFromUISettings(props.channelType);
|
||||
}
|
||||
|
||||
@@ -441,6 +441,7 @@ function reloadState(content = props.modelValue) {
|
||||
}
|
||||
|
||||
function addSignature() {
|
||||
if (props.disabled) return;
|
||||
let content = props.modelValue;
|
||||
// see if the content is empty, if it is before appending the signature
|
||||
// we need to add a paragraph node and move the cursor at the start of the editor
|
||||
@@ -459,6 +460,7 @@ function addSignature() {
|
||||
}
|
||||
|
||||
function removeSignature() {
|
||||
if (props.disabled) return;
|
||||
if (!props.signature) return;
|
||||
let content = props.modelValue;
|
||||
content = removeSignatureHelper(
|
||||
@@ -569,14 +571,10 @@ function updateImgToolbarOnDelete() {
|
||||
}
|
||||
|
||||
function isEnterToSendEnabled() {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
if (window.__WOOT_ISOLATED_SHELL__) return false;
|
||||
return isEditorHotKeyEnabled('enter');
|
||||
}
|
||||
|
||||
function isCmdPlusEnterToSendEnabled() {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
if (window.__WOOT_ISOLATED_SHELL__) return true;
|
||||
return isEditorHotKeyEnabled('cmd_enter');
|
||||
}
|
||||
|
||||
@@ -815,12 +813,12 @@ watch(
|
||||
|
||||
watch(sendWithSignature, newValue => {
|
||||
// see if the allowSignature flag is true
|
||||
if (props.allowSignature) {
|
||||
if (props.allowSignature && !props.disabled) {
|
||||
toggleSignatureInEditor(newValue);
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
onMounted(() => {
|
||||
// [VITE] state assignment was done in created before
|
||||
state = createState(
|
||||
props.modelValue,
|
||||
@@ -832,10 +830,7 @@ onMounted(async () => {
|
||||
|
||||
createEditorView();
|
||||
editorView.updateState(state);
|
||||
// TODO: test this in the main app
|
||||
if (sendWithSignature.value) {
|
||||
addSignature();
|
||||
} else if (props.focusOnMount) {
|
||||
if (props.focusOnMount) {
|
||||
focusEditorInputField();
|
||||
}
|
||||
});
|
||||
@@ -929,10 +924,6 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
|
||||
.ProseMirror-menubar-wrapper {
|
||||
@apply flex flex-col gap-3;
|
||||
|
||||
.ProseMirror-menubar:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ProseMirror-menubar {
|
||||
min-height: 1.25rem !important;
|
||||
@apply items-center gap-4 flex pb-0 bg-transparent text-n-slate-11 relative ltr:-left-[3px] rtl:-right-[3px];
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { computed, useTemplateRef } from 'vue';
|
||||
import { useElementSize } from '@vueuse/core';
|
||||
import { REPLY_EDITOR_MODES } from './constants';
|
||||
import Icon from 'next/icon/Icon.vue';
|
||||
|
||||
const props = defineProps({
|
||||
mode: {
|
||||
@@ -13,10 +12,6 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
iconOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isReplyRestricted: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -74,34 +69,22 @@ const translateValue = computed(() => {
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="flex items-center w-auto p-1 transition-all border rounded-full bg-n-alpha-2 group relative duration-300 ease-in-out z-0 active:scale-[0.995] active:duration-75"
|
||||
class="flex items-center w-auto h-8 p-1 transition-all border rounded-full bg-n-alpha-2 group relative duration-300 ease-in-out z-0 active:scale-[0.995] active:duration-75"
|
||||
:disabled="disabled || isReplyRestricted"
|
||||
:class="{
|
||||
'h-7': iconOnly,
|
||||
'h-8': !iconOnly,
|
||||
'cursor-not-allowed': disabled || isReplyRestricted,
|
||||
}"
|
||||
@click="$emit('toggleMode')"
|
||||
>
|
||||
<div
|
||||
ref="wootEditorReplyMode"
|
||||
class="flex items-center gap-1 px-2 z-20 text-n-slate-11"
|
||||
>
|
||||
<Icon v-if="iconOnly" icon="i-lucide-message-circle" class="size-4" />
|
||||
<template v-else>{{ $t('CONVERSATION.REPLYBOX.REPLY') }}</template>
|
||||
<div ref="wootEditorReplyMode" class="flex items-center gap-1 px-2 z-20">
|
||||
{{ $t('CONVERSATION.REPLYBOX.REPLY') }}
|
||||
</div>
|
||||
<div
|
||||
ref="wootEditorPrivateMode"
|
||||
class="flex items-center gap-1 px-2 z-20 text-n-slate-11"
|
||||
>
|
||||
<Icon v-if="iconOnly" icon="i-lucide-lock-keyhole" class="size-4" />
|
||||
<template v-else>{{ $t('CONVERSATION.REPLYBOX.PRIVATE_NOTE') }}</template>
|
||||
<div ref="wootEditorPrivateMode" class="flex items-center gap-1 px-2 z-20">
|
||||
{{ $t('CONVERSATION.REPLYBOX.PRIVATE_NOTE') }}
|
||||
</div>
|
||||
<div
|
||||
class="absolute shadow-sm rounded-full h-6 w-[var(--chip-width)] ease-in-out translate-x-[var(--translate-x)] rtl:translate-x-[var(--rtl-translate-x)] bg-n-solid-1"
|
||||
:class="{
|
||||
'h-5': iconOnly,
|
||||
'h-6': !iconOnly,
|
||||
'transition-all duration-300': !disabled && !isReplyRestricted,
|
||||
}"
|
||||
:style="{
|
||||
|
||||
@@ -79,7 +79,7 @@ export default {
|
||||
|
||||
created() {
|
||||
state = createState(
|
||||
this.modelValue,
|
||||
this.modelValue || '',
|
||||
this.placeholder,
|
||||
this.plugins,
|
||||
{ onImageUpload: this.openFileBrowser },
|
||||
@@ -170,7 +170,7 @@ export default {
|
||||
},
|
||||
reloadState() {
|
||||
state = createState(
|
||||
this.modelValue,
|
||||
this.modelValue || '',
|
||||
this.placeholder,
|
||||
this.plugins,
|
||||
{ onImageUpload: this.openFileBrowser },
|
||||
|
||||
@@ -114,11 +114,6 @@ export default {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
allowSignature: { type: Boolean, default: false },
|
||||
allowEmoji: { type: Boolean, default: false },
|
||||
allowVideoCall: { type: Boolean, default: false },
|
||||
allowFileUpload: { type: Boolean, default: false },
|
||||
allowAudioRecorder: { type: Boolean, default: false },
|
||||
showQuotedReplyToggle: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -133,7 +128,6 @@ export default {
|
||||
},
|
||||
},
|
||||
emits: [
|
||||
'replaceText',
|
||||
'toggleInsertArticle',
|
||||
'selectWhatsappTemplate',
|
||||
'selectContentTemplate',
|
||||
@@ -282,9 +276,6 @@ export default {
|
||||
toggleMessageSignature() {
|
||||
this.setSignatureFlagForInbox(this.channelType, !this.sendWithSignature);
|
||||
},
|
||||
replaceText(text) {
|
||||
this.$emit('replaceText', text);
|
||||
},
|
||||
toggleInsertArticle() {
|
||||
this.$emit('toggleInsertArticle');
|
||||
},
|
||||
@@ -296,7 +287,7 @@ export default {
|
||||
<div class="flex justify-between p-3" :class="wrapClass">
|
||||
<div class="left-wrap">
|
||||
<NextButton
|
||||
v-if="allowEmoji && !isEditorDisabled"
|
||||
v-if="!isEditorDisabled"
|
||||
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_EMOJI_ICON')"
|
||||
icon="i-ph-smiley-sticker"
|
||||
slate
|
||||
@@ -305,7 +296,7 @@ export default {
|
||||
@click="toggleEmojiPicker"
|
||||
/>
|
||||
<FileUpload
|
||||
v-if="showAttachButton && allowFileUpload"
|
||||
v-if="showAttachButton"
|
||||
ref="uploadRef"
|
||||
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_ATTACH_ICON')"
|
||||
input-id="conversationAttachment"
|
||||
@@ -330,7 +321,7 @@ export default {
|
||||
/>
|
||||
</FileUpload>
|
||||
<NextButton
|
||||
v-if="allowAudioRecorder && showAudioRecorderButton"
|
||||
v-if="showAudioRecorderButton"
|
||||
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_ICON')"
|
||||
:icon="!isRecordingAudio ? 'i-ph-microphone' : 'i-ph-microphone-slash'"
|
||||
slate
|
||||
@@ -339,7 +330,7 @@ export default {
|
||||
@click="toggleAudioRecorder"
|
||||
/>
|
||||
<NextButton
|
||||
v-if="allowAudioRecorder && showAudioPlayStopButton"
|
||||
v-if="showAudioPlayStopButton"
|
||||
:icon="audioRecorderPlayStopIcon"
|
||||
slate
|
||||
faded
|
||||
@@ -348,7 +339,7 @@ export default {
|
||||
@click="toggleAudioRecorderPlayPause"
|
||||
/>
|
||||
<NextButton
|
||||
v-if="showMessageSignatureButton && allowSignature"
|
||||
v-if="showMessageSignatureButton"
|
||||
v-tooltip.top-end="signatureToggleTooltip"
|
||||
icon="i-ph-signature"
|
||||
slate
|
||||
@@ -386,7 +377,6 @@ export default {
|
||||
/>
|
||||
<VideoCallButton
|
||||
v-if="
|
||||
allowVideoCall &&
|
||||
(isAWebWidgetInbox || isAPIInbox) &&
|
||||
!isOnPrivateNote &&
|
||||
!isEditorDisabled
|
||||
|
||||
@@ -25,10 +25,6 @@ export default {
|
||||
type: String,
|
||||
default: REPLY_EDITOR_MODES.REPLY,
|
||||
},
|
||||
disablePopout: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isReplyRestricted: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
||||
@@ -10,7 +10,7 @@ import InboxName from '../InboxName.vue';
|
||||
import ConversationContextMenu from './contextMenu/Index.vue';
|
||||
import TimeAgo from 'dashboard/components/ui/TimeAgo.vue';
|
||||
import CardLabels from './conversationCardComponents/CardLabels.vue';
|
||||
import PriorityMark from './PriorityMark.vue';
|
||||
import CardPriorityIcon from 'dashboard/components-next/Conversation/ConversationCard/CardPriorityIcon.vue';
|
||||
import SLACardLabel from './components/SLACardLabel.vue';
|
||||
import ContextMenu from 'dashboard/components/ui/ContextMenu.vue';
|
||||
import VoiceCallStatus from './VoiceCallStatus.vue';
|
||||
@@ -305,7 +305,7 @@ const deleteConversation = () => {
|
||||
>
|
||||
<InboxName v-if="showInboxName" :inbox="inbox" class="flex-1 min-w-0" />
|
||||
<div
|
||||
class="flex items-center gap-2 flex-shrink-0"
|
||||
class="flex items-baseline gap-2 flex-shrink-0"
|
||||
:class="{
|
||||
'flex-1 justify-between': !showInboxName,
|
||||
}"
|
||||
@@ -317,7 +317,10 @@ const deleteConversation = () => {
|
||||
<fluent-icon icon="person" size="12" class="text-n-slate-11" />
|
||||
{{ assignee.name }}
|
||||
</span>
|
||||
<PriorityMark :priority="chat.priority" class="flex-shrink-0" />
|
||||
<CardPriorityIcon
|
||||
:priority="chat.priority"
|
||||
class="flex-shrink-0 !size-3.5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<h4
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
<script>
|
||||
import { CONVERSATION_PRIORITY } from '../../../../shared/constants/messages';
|
||||
|
||||
export default {
|
||||
name: 'PriorityMark',
|
||||
props: {
|
||||
priority: {
|
||||
type: String,
|
||||
default: '',
|
||||
validate: value =>
|
||||
[...Object.values(CONVERSATION_PRIORITY), ''].includes(value),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
CONVERSATION_PRIORITY,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
tooltipText() {
|
||||
return this.$t(
|
||||
`CONVERSATION.PRIORITY.OPTIONS.${this.priority.toUpperCase()}`
|
||||
);
|
||||
},
|
||||
isUrgent() {
|
||||
return this.priority === CONVERSATION_PRIORITY.URGENT;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
||||
<template>
|
||||
<span
|
||||
v-if="priority"
|
||||
v-tooltip="{
|
||||
content: tooltipText,
|
||||
delay: { show: 1500, hide: 0 },
|
||||
}"
|
||||
class="shrink-0 rounded-sm inline-flex items-center justify-center w-3.5 h-3.5"
|
||||
:class="{
|
||||
'bg-n-ruby-4 text-n-ruby-10': isUrgent,
|
||||
'bg-n-slate-4 text-n-slate-11': !isUrgent,
|
||||
}"
|
||||
>
|
||||
<fluent-icon
|
||||
:icon="`priority-${priority.toLowerCase()}`"
|
||||
:size="isUrgent ? 12 : 14"
|
||||
class="flex-shrink-0"
|
||||
view-box="0 0 14 14"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
@@ -27,7 +27,6 @@ import { CMD_AI_ASSIST } from 'dashboard/helper/commandbar/events';
|
||||
import {
|
||||
getMessageVariables,
|
||||
getUndefinedVariablesInMessage,
|
||||
replaceVariablesInMessage,
|
||||
} from '@chatwoot/utils';
|
||||
import WhatsappTemplates from './WhatsappTemplates/Modal.vue';
|
||||
import ContentTemplates from './ContentTemplates/ContentTemplatesModal.vue';
|
||||
@@ -253,6 +252,9 @@ export default {
|
||||
if (this.isAnInstagramChannel) {
|
||||
return MESSAGE_MAX_LENGTH.INSTAGRAM;
|
||||
}
|
||||
if (this.isATelegramChannel) {
|
||||
return MESSAGE_MAX_LENGTH.TELEGRAM;
|
||||
}
|
||||
if (this.isATiktokChannel) {
|
||||
return MESSAGE_MAX_LENGTH.TIKTOK;
|
||||
}
|
||||
@@ -545,7 +547,10 @@ export default {
|
||||
},
|
||||
setCopilotAcceptedMessage(message, replyType = this.replyType) {
|
||||
const key = this.getDraftKey(this.conversationIdByRoute, replyType);
|
||||
this.copilotAcceptedMessages[key] = trimContent(message || '');
|
||||
this.copilotAcceptedMessages[key] = trimContent(
|
||||
message || '',
|
||||
this.maxLength
|
||||
);
|
||||
},
|
||||
clearCopilotAcceptedMessage(replyType = this.replyType) {
|
||||
const key = this.getDraftKey(this.conversationIdByRoute, replyType);
|
||||
@@ -603,7 +608,7 @@ export default {
|
||||
saveDraft(conversationId, replyType) {
|
||||
if (this.message || this.message === '') {
|
||||
const key = this.getDraftKey(conversationId, replyType);
|
||||
const draftToSave = trimContent(this.message || '');
|
||||
const draftToSave = trimContent(this.message || '', this.maxLength);
|
||||
|
||||
this.$store.dispatch('draftMessages/set', {
|
||||
key,
|
||||
@@ -630,10 +635,17 @@ export default {
|
||||
return message;
|
||||
}
|
||||
|
||||
// Even when editor is disabled (e.g. WhatsApp/API can't reply), we must
|
||||
// still normalize stale signatures out of drafts when signature is off.
|
||||
if (this.isEditorDisabled && this.sendWithSignature) {
|
||||
return message;
|
||||
}
|
||||
|
||||
const effectiveChannelType = getEffectiveChannelType(
|
||||
this.channelType,
|
||||
this.inbox?.medium || ''
|
||||
);
|
||||
|
||||
return this.sendWithSignature
|
||||
? appendSignature(message, this.messageSignature, effectiveChannelType)
|
||||
: removeSignature(message, this.messageSignature, effectiveChannelType);
|
||||
@@ -905,32 +917,6 @@ export default {
|
||||
});
|
||||
this.hideContentTemplatesModal();
|
||||
},
|
||||
replaceText(message) {
|
||||
if (this.sendWithSignature && !this.private) {
|
||||
// if signature is enabled, append it to the message
|
||||
// appendSignature ensures that the signature is not duplicated
|
||||
// so we don't need to check if the signature is already present
|
||||
const effectiveChannelType = getEffectiveChannelType(
|
||||
this.channelType,
|
||||
this.inbox?.medium || ''
|
||||
);
|
||||
message = appendSignature(
|
||||
message,
|
||||
this.messageSignature,
|
||||
effectiveChannelType
|
||||
);
|
||||
}
|
||||
|
||||
const updatedMessage = replaceVariablesInMessage({
|
||||
message,
|
||||
variables: this.messageVariables,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
useTrack(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE);
|
||||
this.message = updatedMessage;
|
||||
}, 100);
|
||||
},
|
||||
setReplyMode(mode = REPLY_EDITOR_MODES.REPLY) {
|
||||
// Clear attachments when switching between private note and reply modes
|
||||
// This is to prevent from breaking the upload rules
|
||||
@@ -1429,7 +1415,6 @@ export default {
|
||||
:new-conversation-modal-active="newConversationModalActive"
|
||||
@select-whatsapp-template="openWhatsappTemplateModal"
|
||||
@select-content-template="openContentTemplateModal"
|
||||
@replace-text="replaceText"
|
||||
@toggle-insert-article="toggleInsertArticle"
|
||||
@toggle-quoted-reply="toggleQuotedReply"
|
||||
/>
|
||||
|
||||
@@ -9,7 +9,6 @@ import { useImageZoom } from 'dashboard/composables/useImageZoom';
|
||||
import { messageTimestamp } from 'shared/helpers/timeHelper';
|
||||
import { downloadFile } from '@chatwoot/utils';
|
||||
|
||||
import WootModal from 'dashboard/components/Modal.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import Avatar from 'next/avatar/Avatar.vue';
|
||||
import TeleportWithDirection from 'dashboard/components-next/TeleportWithDirection.vue';
|
||||
@@ -169,7 +168,7 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<TeleportWithDirection to="body">
|
||||
<WootModal
|
||||
<woot-modal
|
||||
v-model:show="show"
|
||||
full-width
|
||||
:show-close-button="false"
|
||||
@@ -212,7 +211,7 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex-1 mx-2 px-2 truncate text-sm font-medium text-center text-n-slate-12 hidden sm:block"
|
||||
class="flex-1 mx-2 px-2 truncate text-sm font-medium text-center text-n-slate-12"
|
||||
>
|
||||
<span v-dompurify-html="fileNameFromDataUrl" class="truncate" />
|
||||
</div>
|
||||
@@ -354,6 +353,6 @@ onMounted(() => {
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</WootModal>
|
||||
</woot-modal>
|
||||
</TeleportWithDirection>
|
||||
</template>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
agents,
|
||||
teams,
|
||||
labels,
|
||||
booleanFilterOptions,
|
||||
statusFilterOptions,
|
||||
messageTypeOptions,
|
||||
priorityOptions,
|
||||
@@ -73,6 +74,8 @@ describe('useAutomation', () => {
|
||||
return countries;
|
||||
case 'message_type':
|
||||
return messageTypeOptions;
|
||||
case 'private_note':
|
||||
return booleanFilterOptions;
|
||||
case 'priority':
|
||||
return priorityOptions;
|
||||
default:
|
||||
@@ -226,6 +229,9 @@ describe('useAutomation', () => {
|
||||
expect(getConditionDropdownValues('message_type')).toEqual(
|
||||
messageTypeOptions
|
||||
);
|
||||
expect(getConditionDropdownValues('private_note')).toEqual(
|
||||
booleanFilterOptions
|
||||
);
|
||||
expect(getConditionDropdownValues('priority')).toEqual(priorityOptions);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useEditableAutomation } from '../useEditableAutomation';
|
||||
import useAutomationValues from '../useAutomationValues';
|
||||
|
||||
vi.mock('../useAutomationValues');
|
||||
|
||||
describe('useEditableAutomation', () => {
|
||||
beforeEach(() => {
|
||||
useAutomationValues.mockReturnValue({
|
||||
getConditionDropdownValues: vi.fn(attributeKey => {
|
||||
if (attributeKey === 'private_note') {
|
||||
return [
|
||||
{ id: true, name: 'True' },
|
||||
{ id: false, name: 'False' },
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}),
|
||||
getActionDropdownValues: vi.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
it('rehydrates boolean conditions as a single selected option', () => {
|
||||
const automation = {
|
||||
event_name: 'message_created',
|
||||
conditions: [
|
||||
{
|
||||
attribute_key: 'private_note',
|
||||
filter_operator: 'equal_to',
|
||||
values: [false],
|
||||
query_operator: null,
|
||||
},
|
||||
],
|
||||
actions: [],
|
||||
};
|
||||
const automationTypes = {
|
||||
message_created: {
|
||||
conditions: [{ key: 'private_note', inputType: 'search_select' }],
|
||||
},
|
||||
};
|
||||
|
||||
const { formatAutomation } = useEditableAutomation();
|
||||
const result = formatAutomation(automation, [], automationTypes, []);
|
||||
|
||||
expect(result.conditions).toEqual([
|
||||
{
|
||||
attribute_key: 'private_note',
|
||||
filter_operator: 'equal_to',
|
||||
values: { id: false, name: 'False' },
|
||||
query_operator: 'and',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -2,12 +2,6 @@ import { computed, unref } from 'vue';
|
||||
import { getCurrentInstance } from 'vue';
|
||||
|
||||
export const useStore = () => {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
if (window.__CHATWOOT_STORE__) {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
return window.__CHATWOOT_STORE__;
|
||||
}
|
||||
|
||||
const vm = getCurrentInstance();
|
||||
if (!vm) throw new Error('must be called in setup');
|
||||
return vm.proxy.$store;
|
||||
|
||||
@@ -11,12 +11,7 @@ export function useAccount() {
|
||||
* Computed property for the current account ID.
|
||||
* @type {import('vue').ComputedRef<number>}
|
||||
*/
|
||||
// In the isolated shell (react-components / Web Component context),
|
||||
// vue-router is not available. Fall back to the global variable.
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const isIsolatedShell = !!window.__WOOT_ISOLATED_SHELL__;
|
||||
const route = isIsolatedShell ? null : useRoute();
|
||||
|
||||
const route = useRoute();
|
||||
const store = useStore();
|
||||
const getAccountFn = useMapGetter('accounts/getAccount');
|
||||
const isOnChatwootCloud = useMapGetter('globalConfig/isOnChatwootCloud');
|
||||
@@ -25,10 +20,6 @@ export function useAccount() {
|
||||
);
|
||||
|
||||
const accountId = computed(() => {
|
||||
if (isIsolatedShell) {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
return Number(window.__WOOT_ACCOUNT_ID__);
|
||||
}
|
||||
return Number(route.params.accountId);
|
||||
});
|
||||
const currentAccount = computed(() => getAccountFn.value(accountId.value));
|
||||
|
||||
@@ -46,11 +46,26 @@ export function useEditableAutomation() {
|
||||
if (inputType === 'comma_separated_plain_text') {
|
||||
return { ...condition, values: condition.values.join(',') };
|
||||
}
|
||||
const dropdownValues = getConditionDropdownValues(
|
||||
condition.attribute_key
|
||||
);
|
||||
const hasBooleanOptions =
|
||||
inputType === 'search_select' &&
|
||||
dropdownValues.length &&
|
||||
dropdownValues.every(item => typeof item.id === 'boolean');
|
||||
|
||||
if (hasBooleanOptions) {
|
||||
return {
|
||||
...condition,
|
||||
query_operator: condition.query_operator || 'and',
|
||||
values: dropdownValues.find(item => item.id === condition.values[0]),
|
||||
};
|
||||
}
|
||||
return {
|
||||
...condition,
|
||||
query_operator: condition.query_operator || 'and',
|
||||
values: [...getConditionDropdownValues(condition.attribute_key)].filter(
|
||||
item => [...condition.values].includes(item.id)
|
||||
values: [...dropdownValues].filter(item =>
|
||||
[...condition.values].includes(item.id)
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -37,6 +37,7 @@ export const FEATURE_FLAGS = {
|
||||
CHANNEL_INSTAGRAM: 'channel_instagram',
|
||||
CHANNEL_TIKTOK: 'channel_tiktok',
|
||||
CONTACT_CHATWOOT_SUPPORT_TEAM: 'contact_chatwoot_support_team',
|
||||
CAPTAIN_CUSTOM_TOOLS: 'custom_tools',
|
||||
CAPTAIN_V2: 'captain_integration_v2',
|
||||
CAPTAIN_TASKS: 'captain_tasks',
|
||||
SAML: 'saml',
|
||||
@@ -49,6 +50,7 @@ export const FEATURE_FLAGS = {
|
||||
export const PREMIUM_FEATURES = [
|
||||
FEATURE_FLAGS.SLA,
|
||||
FEATURE_FLAGS.CAPTAIN,
|
||||
FEATURE_FLAGS.CAPTAIN_CUSTOM_TOOLS,
|
||||
FEATURE_FLAGS.CUSTOM_ROLES,
|
||||
FEATURE_FLAGS.AUDIT_LOGS,
|
||||
FEATURE_FLAGS.HELP_CENTER,
|
||||
|
||||
@@ -10,10 +10,7 @@ const { isImpersonating } = useImpersonation();
|
||||
class ActionCableConnector extends BaseActionCableConnector {
|
||||
constructor(app, pubsubToken) {
|
||||
const { websocketURL = '' } = window.chatwootConfig || {};
|
||||
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const wsURL = websocketURL || window.__WEBSOCKET_URL__ || '';
|
||||
super(app, pubsubToken, wsURL);
|
||||
super(app, pubsubToken, websocketURL);
|
||||
this.CancelTyping = [];
|
||||
this.events = {
|
||||
'message.created': this.onMessageCreated,
|
||||
@@ -101,10 +98,7 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||
conversation: { last_activity_at: lastActivityAt },
|
||||
conversation_id: conversationId,
|
||||
} = data;
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
if (!window.__WOOT_ISOLATED_SHELL__) {
|
||||
DashboardAudioNotificationHelper.onNewMessage(data);
|
||||
}
|
||||
DashboardAudioNotificationHelper.onNewMessage(data);
|
||||
this.app.$store.dispatch('addMessage', data);
|
||||
this.app.$store.dispatch('updateConversationLastActivity', {
|
||||
lastActivityAt,
|
||||
|
||||
@@ -150,6 +150,7 @@ export const getConditionOptions = ({
|
||||
conversation_language: languages,
|
||||
country_code: countries,
|
||||
message_type: messageTypeOptions,
|
||||
private_note: booleanFilterOptions,
|
||||
priority: priorityOptions,
|
||||
labels: generateConditionOptions(labels, 'title'),
|
||||
};
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
/* eslint no-param-reassign: 0 */
|
||||
|
||||
import getUuid from 'widget/helpers/uuid';
|
||||
import { MESSAGE_STATUS, MESSAGE_TYPE } from 'shared/constants/messages';
|
||||
|
||||
export default () => {
|
||||
if (!Array.prototype.last) {
|
||||
Object.assign(Array.prototype, {
|
||||
last() {
|
||||
return this[this.length - 1];
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const isEmptyObject = obj =>
|
||||
Object.keys(obj).length === 0 && obj.constructor === Object;
|
||||
|
||||
|
||||
@@ -32,6 +32,25 @@ export function extractTextFromMarkdown(markdown) {
|
||||
.trim(); // Trim any extra space
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes inline base64 markdown images from signature content.
|
||||
*
|
||||
* @param {string} content
|
||||
* @returns {{ sanitizedContent: string, hasInlineImages: boolean }}
|
||||
*/
|
||||
export function stripInlineBase64Images(content) {
|
||||
if (!content || typeof content !== 'string') {
|
||||
return { sanitizedContent: content || '', hasInlineImages: false };
|
||||
}
|
||||
|
||||
const markdownInlineBase64ImageRegex =
|
||||
/!\[[^\]]*]\(\s*data:image\/[a-zA-Z0-9.+-]+;base64,[^)]+\s*\)/gi;
|
||||
const sanitizedContent = content.replace(markdownInlineBase64ImageRegex, '');
|
||||
const hasInlineImages = sanitizedContent !== content;
|
||||
|
||||
return { sanitizedContent, hasInlineImages };
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip unsupported markdown formatting based on channel capabilities.
|
||||
* Uses MARKDOWN_PATTERNS from editor constants.
|
||||
|
||||
@@ -166,6 +166,8 @@ const TOD_TO_MERIDIEM = {
|
||||
evening: 'pm',
|
||||
night: 'pm',
|
||||
};
|
||||
const CJK_CHAR_RE =
|
||||
/[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
|
||||
|
||||
// ─── Translation Cache ──────────────────────────────────────────────────────
|
||||
|
||||
@@ -278,8 +280,13 @@ const escapeRegex = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const substituteLocalTokens = (text, pairs) => {
|
||||
let r = text;
|
||||
pairs.forEach(([local, en]) => {
|
||||
const re = new RegExp(`(?<=^|\\s)${escapeRegex(local)}(?=\\s|$)`, 'g');
|
||||
r = r.replace(re, en);
|
||||
if (CJK_CHAR_RE.test(local)) {
|
||||
const re = new RegExp(escapeRegex(local), 'g');
|
||||
r = r.replace(re, ` ${en} `);
|
||||
} else {
|
||||
const re = new RegExp(`(?<=^|\\s)${escapeRegex(local)}(?=\\s|$)`, 'g');
|
||||
r = r.replace(re, en);
|
||||
}
|
||||
});
|
||||
return r;
|
||||
};
|
||||
|
||||
@@ -82,6 +82,9 @@ const ORDINAL_RE = `(\\d{1,2}(?:st|nd|rd|th)?|${ORDINAL_WORDS})`;
|
||||
|
||||
const HALF_UNIT_RE = /^(?:in\s+)?half\s+(?:an?\s+)?(hour|day|week|month|year)$/;
|
||||
const RELATIVE_DURATION_RE = new RegExp(`^(?:in\\s+)?${NUM_RE}\\s+${UNIT_RE}$`);
|
||||
const RELATIVE_DURATION_AFTER_RE = new RegExp(
|
||||
`^(?:in\\s+)?${NUM_RE}\\s+${UNIT_RE}\\s+after$`
|
||||
);
|
||||
const DURATION_FROM_NOW_RE = new RegExp(
|
||||
`^${NUM_RE}\\s+${UNIT_RE}\\s+from\\s+now$`
|
||||
);
|
||||
@@ -89,6 +92,9 @@ const RELATIVE_DAY_ONLY_RE = new RegExp(`^(${RELATIVE_DAYS})$`);
|
||||
const RELATIVE_DAY_TOD_RE = new RegExp(
|
||||
`^(${RELATIVE_DAYS})\\s+(?:at\\s+)?(${TIME_OF_DAY_NAMES})$`
|
||||
);
|
||||
const RELATIVE_DAY_MERIDIEM_RE = new RegExp(
|
||||
`^(${RELATIVE_DAYS})\\s+(?:at\\s+)?(am|pm)$`
|
||||
);
|
||||
const RELATIVE_DAY_TOD_TIME_RE = new RegExp(
|
||||
`^(${RELATIVE_DAYS})\\s+(?:at\\s+)?(${TIME_OF_DAY_NAMES})\\s+(\\d{1,2}(?::\\d{2})?)$`
|
||||
);
|
||||
@@ -245,6 +251,7 @@ const matchDuration = (text, now) => {
|
||||
|
||||
return (
|
||||
parseDuration(text.match(DURATION_FROM_NOW_RE), now) ||
|
||||
parseDuration(text.match(RELATIVE_DURATION_AFTER_RE), now) ||
|
||||
parseDuration(text.match(RELATIVE_DURATION_RE), now)
|
||||
);
|
||||
};
|
||||
@@ -303,6 +310,13 @@ const matchRelativeDay = (text, now) => {
|
||||
);
|
||||
}
|
||||
|
||||
const dayMeridiemMatch = text.match(RELATIVE_DAY_MERIDIEM_RE);
|
||||
if (dayMeridiemMatch) {
|
||||
const [, dayKey, meridiem] = dayMeridiemMatch;
|
||||
const hours = meridiem === 'am' ? 9 : 14;
|
||||
return applyTimeWithRollover(RELATIVE_DAY_MAP[dayKey], hours, 0, now);
|
||||
}
|
||||
|
||||
const dayAtTimeMatch = text.match(RELATIVE_DAY_AT_TIME_RE);
|
||||
if (dayAtTimeMatch) {
|
||||
const [, dayKey, timeRaw] = dayAtTimeMatch;
|
||||
|
||||
@@ -178,6 +178,21 @@ describe('getConditionOptions', () => {
|
||||
})
|
||||
).toEqual(testOptions);
|
||||
});
|
||||
|
||||
it('returns boolean options for private_note', () => {
|
||||
const booleanOptions = [
|
||||
{ id: true, name: 'True' },
|
||||
{ id: false, name: 'False' },
|
||||
];
|
||||
|
||||
expect(
|
||||
helpers.getConditionOptions({
|
||||
booleanFilterOptions: booleanOptions,
|
||||
customAttributes,
|
||||
type: 'private_note',
|
||||
})
|
||||
).toEqual(booleanOptions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFileName', () => {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
getMenuAnchor,
|
||||
calculateMenuPosition,
|
||||
stripUnsupportedFormatting,
|
||||
stripInlineBase64Images,
|
||||
} from '../editorHelper';
|
||||
import { FORMATTING } from 'dashboard/constants/editor';
|
||||
import { EditorState } from '@chatwoot/prosemirror-schema';
|
||||
@@ -423,6 +424,36 @@ describe('extractTextFromMarkdown', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('stripInlineBase64Images', () => {
|
||||
it('removes markdown data:image base64 images and sets hasInlineImages', () => {
|
||||
const content =
|
||||
'Hello\n\nWorld';
|
||||
const { sanitizedContent, hasInlineImages } =
|
||||
stripInlineBase64Images(content);
|
||||
|
||||
expect(hasInlineImages).toBe(true);
|
||||
expect(sanitizedContent).not.toContain('data:image/png;base64');
|
||||
expect(sanitizedContent).toContain('Hello');
|
||||
expect(sanitizedContent).toContain('World');
|
||||
});
|
||||
|
||||
it('leaves hosted image markdown unchanged', () => {
|
||||
const content = '';
|
||||
const { sanitizedContent, hasInlineImages } =
|
||||
stripInlineBase64Images(content);
|
||||
|
||||
expect(hasInlineImages).toBe(false);
|
||||
expect(sanitizedContent).toBe(content);
|
||||
});
|
||||
|
||||
it('returns empty hasInlineImages for empty input', () => {
|
||||
expect(stripInlineBase64Images('')).toEqual({
|
||||
sanitizedContent: '',
|
||||
hasInlineImages: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('insertAtCursor', () => {
|
||||
it('should return undefined if editorView is not provided', () => {
|
||||
const result = insertAtCursor(undefined, schema.text('Hello'), 0);
|
||||
|
||||
@@ -1626,6 +1626,24 @@ describe('generateDateSuggestions — localized input regressions', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const zhTWSnoozeTranslations = {
|
||||
UNITS: {
|
||||
HOUR: '小時',
|
||||
HOURS: '小時',
|
||||
DAY: '天',
|
||||
DAYS: '天',
|
||||
},
|
||||
HALF: '半',
|
||||
RELATIVE: {
|
||||
TOMORROW: '明天',
|
||||
},
|
||||
MERIDIEM: {
|
||||
AM: '上午',
|
||||
PM: '下午',
|
||||
},
|
||||
AFTER: '後',
|
||||
};
|
||||
|
||||
describe('P1: short non-English tokens must NOT produce spurious half-duration suggestions', () => {
|
||||
it('Arabic "غد" does not produce half-duration suggestions', () => {
|
||||
const results = generateDateSuggestions('غد', now, {
|
||||
@@ -1721,6 +1739,37 @@ describe('generateDateSuggestions — localized input regressions', () => {
|
||||
expect(results[0].date.getHours()).toBe(6);
|
||||
});
|
||||
});
|
||||
|
||||
describe('zh_TW compact CJK inputs', () => {
|
||||
const options = {
|
||||
translations: zhTWSnoozeTranslations,
|
||||
locale: 'zh-TW',
|
||||
};
|
||||
|
||||
it('parses "2小時後" (2 hours from now) without spaces', () => {
|
||||
const results = generateDateSuggestions('2小時後', now, options);
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
expect(results[0].date.getDate()).toBe(16);
|
||||
expect(results[0].date.getHours()).toBe(12);
|
||||
expect(results[0].date.getMinutes()).toBe(0);
|
||||
});
|
||||
|
||||
it('parses "半天" (half day) without spaces', () => {
|
||||
const results = generateDateSuggestions('半天', now, options);
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
expect(results[0].date.getDate()).toBe(16);
|
||||
expect(results[0].date.getHours()).toBe(22);
|
||||
expect(results[0].date.getMinutes()).toBe(0);
|
||||
});
|
||||
|
||||
it('parses "明天 上午" (tomorrow AM) into tomorrow 9am', () => {
|
||||
const results = generateDateSuggestions('明天 上午', now, options);
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
expect(results[0].date.getDate()).toBe(17);
|
||||
expect(results[0].date.getHours()).toBe(9);
|
||||
expect(results[0].date.getMinutes()).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('no-space duration suggestions', () => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,12 +6,12 @@
|
||||
"SWITCH_VIEW_LAYOUT": "Switch the layout",
|
||||
"DASHBOARD_APP_TAB_MESSAGES": "Messages",
|
||||
"UNVERIFIED_SESSION": "The identity of this user is not verified",
|
||||
"NO_MESSAGE_1": "Uh oh! Looks like there are no messages from customers in your inbox.",
|
||||
"NO_MESSAGE_2": " to send a message to your page!",
|
||||
"NO_INBOX_1": "Hola! Looks like you haven't added any inboxes yet.",
|
||||
"NO_INBOX_2": " to get started",
|
||||
"NO_INBOX_AGENT": "Uh Oh! Looks like you are not part of any inbox. Please contact your administrator",
|
||||
"SEARCH_MESSAGES": "Search for messages in conversations",
|
||||
"NO_MESSAGE_1": "የደንበኞች መልእክቶች በኢንቦክስዎ አልተገኙም።",
|
||||
"NO_MESSAGE_2": " ወደ ገፅዎ መልእክት ለመላክ!",
|
||||
"NO_INBOX_1": "እሺ! አሁን ምንም ኢንቦክስ አልጨመሩም።",
|
||||
"NO_INBOX_2": " ለመጀመር",
|
||||
"NO_INBOX_AGENT": "ወይ! ምንም ኢንቦክስ አባል አይደለህም። እባክዎ አስተዳዳሪዎን ያነጋግሩ",
|
||||
"SEARCH_MESSAGES": "መልእክቶችን በውይይቶች ውስጥ ይፈልጉ",
|
||||
"VIEW_ORIGINAL": "View original",
|
||||
"VIEW_TRANSLATED": "View translated",
|
||||
"EMPTY_STATE": {
|
||||
@@ -19,19 +19,19 @@
|
||||
"KEYBOARD_SHORTCUTS": "to view keyboard shortcuts"
|
||||
},
|
||||
"SEARCH": {
|
||||
"TITLE": "Search messages",
|
||||
"TITLE": "መልእክቶችን ይፈልጉ",
|
||||
"RESULT_TITLE": "Search Results",
|
||||
"LOADING_MESSAGE": "Crunching data...",
|
||||
"LOADING_MESSAGE": "መረጃ በማስተናገድ ላይ...",
|
||||
"PLACEHOLDER": "Type any text to search messages",
|
||||
"NO_MATCHING_RESULTS": "No results found."
|
||||
},
|
||||
"UNREAD_MESSAGES": "Unread Messages",
|
||||
"UNREAD_MESSAGE": "Unread Message",
|
||||
"CLICK_HERE": "Click here",
|
||||
"LOADING_INBOXES": "Loading inboxes",
|
||||
"LOADING_CONVERSATIONS": "Loading Conversations",
|
||||
"CANNOT_REPLY": "You cannot reply due to",
|
||||
"24_HOURS_WINDOW": "24 hour message window restriction",
|
||||
"CLICK_HERE": "እዚህ ጠቅ ያድርጉ",
|
||||
"LOADING_INBOXES": "ኢንቦክሶች በመጫን ላይ",
|
||||
"LOADING_CONVERSATIONS": "ውይይቶች በመጫን ላይ",
|
||||
"CANNOT_REPLY": "ምክንያቱን በመነሳት መልስ ማድረግ አይችሉም",
|
||||
"24_HOURS_WINDOW": "የ24 ሰዓት መልእክት ጊዜ ገደብ",
|
||||
"48_HOURS_WINDOW": "48 hour message window restriction",
|
||||
"API_HOURS_WINDOW": "ለዚህ ውይይት መመለስ በ{hours} ሰአታት ውስጥ ብቻ ይቻላል",
|
||||
"NOT_ASSIGNED_TO_YOU": "This conversation is not assigned to you. Would you like to assign this conversation to yourself?",
|
||||
@@ -44,9 +44,9 @@
|
||||
"TWILIO_WHATSAPP_CAN_REPLY": "You can only reply to this conversation using a template message due to",
|
||||
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "24 hour message window restriction",
|
||||
"OLD_INSTAGRAM_INBOX_REPLY_BANNER": "ይህ የInstagram መለያ ወደ አዲሱ የInstagram ቻናል ገቢ ሳጥን ተዛውሯል። ሁሉም አዲስ መልዕክቶች በዚያ ይታያሉ። ከአሁን ጀምሮ ከዚህ ውይይት መልዕክቶች መላክ አትችሉም።",
|
||||
"REPLYING_TO": "You are replying to:",
|
||||
"REMOVE_SELECTION": "Remove Selection",
|
||||
"DOWNLOAD": "Download",
|
||||
"REPLYING_TO": "ለዚህ ትመልሳለህ፦",
|
||||
"REMOVE_SELECTION": "ምርጫ አስወግድ",
|
||||
"DOWNLOAD": "አውርድ",
|
||||
"UNKNOWN_FILE_TYPE": "Unknown File",
|
||||
"SAVE_CONTACT": "Save Contact",
|
||||
"NO_CONTENT": "No content to display",
|
||||
@@ -85,13 +85,13 @@
|
||||
"YOU_ANSWERED": "You answered"
|
||||
},
|
||||
"HEADER": {
|
||||
"RESOLVE_ACTION": "Resolve",
|
||||
"REOPEN_ACTION": "Reopen",
|
||||
"RESOLVE_ACTION": "ተፈትኗል",
|
||||
"REOPEN_ACTION": "እንደገና ክፈት",
|
||||
"OPEN_ACTION": "Open",
|
||||
"MORE_ACTIONS": "ተጨማሪ እርምጃዎች",
|
||||
"OPEN": "More",
|
||||
"CLOSE": "Close",
|
||||
"DETAILS": "details",
|
||||
"OPEN": "ተጨማሪ",
|
||||
"CLOSE": "ዝጋ",
|
||||
"DETAILS": "ዝርዝሮች",
|
||||
"SNOOZED_UNTIL": "Snoozed until",
|
||||
"SNOOZED_UNTIL_TOMORROW": "Snoozed until tomorrow",
|
||||
"SNOOZED_UNTIL_NEXT_WEEK": "Snoozed until next week",
|
||||
@@ -188,21 +188,21 @@
|
||||
"MESSAGE_SIGN_TOOLTIP": "Message signature",
|
||||
"ENABLE_SIGN_TOOLTIP": "Enable signature",
|
||||
"DISABLE_SIGN_TOOLTIP": "Disable signature",
|
||||
"MSG_INPUT": "Shift + enter for new line. Start with '/' to select a Canned Response.",
|
||||
"PRIVATE_MSG_INPUT": "Shift + enter for new line. This will be visible only to Agents",
|
||||
"MSG_INPUT": "አዲስ መስመር ለማስገባት Shift + enter ይጠቀሙ። '/' በመጀመር የተዘጋጀ ምላሽ ይምረጡ።",
|
||||
"PRIVATE_MSG_INPUT": "አዲስ መስመር ለማስገባት Shift + enter ይጠቀሙ። ይህ ለወኪሎች ብቻ ይታያል",
|
||||
"MESSAGING_RESTRICTED": "You cannot reply to this conversation",
|
||||
"MESSAGING_RESTRICTED_WHATSAPP": "You can only reply using a template message due to 24-hour message window restriction",
|
||||
"MESSAGING_RESTRICTED_API": "You can only reply using a template message due to message window restriction",
|
||||
"MESSAGE_SIGNATURE_NOT_CONFIGURED": "Message signature is not configured, please configure it in profile settings.",
|
||||
"COPILOT_MSG_INPUT": "Give copilot additional prompts, or ask anything else... Press enter to send follow-up",
|
||||
"COPILOT_MSG_INPUT": "ኮፒሎት ተጨማሪ እባብነቶች ስጡው, ወይም ሌላ ማንኛውንም ጥያቄ ያቀርቡ... ተከትሎ ለማስተላለፊያ ኤንተር ይጫኑ።",
|
||||
"CLICK_HERE": "Click here to update",
|
||||
"WHATSAPP_TEMPLATES": "Whatsapp Templates"
|
||||
},
|
||||
"REPLYBOX": {
|
||||
"REPLY": "Reply",
|
||||
"PRIVATE_NOTE": "Private Note",
|
||||
"SEND": "Send",
|
||||
"CREATE": "Add Note",
|
||||
"REPLY": "መልስ",
|
||||
"PRIVATE_NOTE": "የግል ማስታወሻ",
|
||||
"SEND": "ላክ",
|
||||
"CREATE": "ማስታወሻ አክል",
|
||||
"INSERT_READ_MORE": "Read more",
|
||||
"DISMISS_REPLY": "Dismiss reply",
|
||||
"REPLYING_TO": "Replying to:",
|
||||
@@ -214,7 +214,7 @@
|
||||
"DRAG_DROP": "Drag and drop here to attach",
|
||||
"START_AUDIO_RECORDING": "Start audio recording",
|
||||
"STOP_AUDIO_RECORDING": "Stop audio recording",
|
||||
"COPILOT_THINKING": "Copilot is thinking",
|
||||
"COPILOT_THINKING": "ኮፒሎት እየሰማራ ነው",
|
||||
"EMAIL_HEAD": {
|
||||
"TO": "TO",
|
||||
"ADD_BCC": "Add bcc",
|
||||
@@ -245,10 +245,10 @@
|
||||
"EXPAND": "Expand preview"
|
||||
}
|
||||
},
|
||||
"VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team",
|
||||
"CHANGE_STATUS": "Conversation status changed",
|
||||
"VISIBLE_TO_AGENTS": "የግል ማስታወሻ፡ ለአንተና ቡድንህ ብቻ ይታያል",
|
||||
"CHANGE_STATUS": "የውይይቱ ሁኔታ ተቀይሯል",
|
||||
"CHANGE_STATUS_FAILED": "Conversation status change failed",
|
||||
"CHANGE_AGENT": "Conversation Assignee changed",
|
||||
"CHANGE_AGENT": "የውይይቱ ተመድብ ተቀይሯል",
|
||||
"CHANGE_AGENT_FAILED": "Assignee change failed",
|
||||
"ASSIGN_LABEL_SUCCESFUL": "Label assigned successfully",
|
||||
"ASSIGN_LABEL_FAILED": "Label assignment failed",
|
||||
@@ -300,20 +300,20 @@
|
||||
}
|
||||
},
|
||||
"EMAIL_TRANSCRIPT": {
|
||||
"TITLE": "Send conversation transcript",
|
||||
"DESC": "Send a copy of the conversation transcript to the specified email address",
|
||||
"SUBMIT": "Submit",
|
||||
"CANCEL": "Cancel",
|
||||
"SEND_EMAIL_SUCCESS": "The chat transcript was sent successfully",
|
||||
"SEND_EMAIL_ERROR": "There was an error, please try again",
|
||||
"TITLE": "የውይይት ጽሑፍ ላክ",
|
||||
"DESC": "የውይይቱን ጽሑፍ ቅጂ ወደ ተጠቃሚው ኢሜይል ላክ",
|
||||
"SUBMIT": "አስገባ",
|
||||
"CANCEL": "ይቅር",
|
||||
"SEND_EMAIL_SUCCESS": "የቻት አጭር መግለጫው በተሳካ ሁኔታ ተልኳል",
|
||||
"SEND_EMAIL_ERROR": "ስህተት ተፈጥሯል፣ እባክዎ ደግመው ይሞክሩ",
|
||||
"SEND_EMAIL_PAYMENT_REQUIRED": "Email transcript is not available on your current plan. Please upgrade to use this feature.",
|
||||
"FORM": {
|
||||
"SEND_TO_CONTACT": "Send the transcript to the customer",
|
||||
"SEND_TO_CONTACT": "መግለጫውን ለደንበኛው ይላኩ",
|
||||
"SEND_TO_AGENT": "Send the transcript to the assigned agent",
|
||||
"SEND_TO_OTHER_EMAIL_ADDRESS": "Send the transcript to another email address",
|
||||
"SEND_TO_OTHER_EMAIL_ADDRESS": "መግለጫውን ወደ ሌላ ኢሜይል አድራሻ ይላኩ",
|
||||
"EMAIL": {
|
||||
"PLACEHOLDER": "Enter an email address",
|
||||
"ERROR": "Please enter a valid email address"
|
||||
"PLACEHOLDER": "ኢሜይል አድራሻ ያስገቡ",
|
||||
"ERROR": "ትክክለኛ ኢሜይል አድራሻ ያስገቡ"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,39 +1,39 @@
|
||||
{
|
||||
"REPORT": {
|
||||
"HEADER": "Conversations",
|
||||
"LOADING_CHART": "Loading chart data...",
|
||||
"NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.",
|
||||
"LOADING_CHART": "ገበታ ውሂብ በማስጫን ላይ...",
|
||||
"NO_ENOUGH_DATA": "ሪፖርት ለማመንጨት በቂ ውሂብ ነጥቦች አልደረሰንም፣ እባክዎ በኋላ ደግመው ይሞክሩ።",
|
||||
"DOWNLOAD_CONVERSATION_REPORTS": "Download conversation reports",
|
||||
"DATA_FETCHING_FAILED": "Failed to fetch data, please try again later.",
|
||||
"SUMMARY_FETCHING_FAILED": "Failed to fetch summary, please try again later.",
|
||||
"METRICS": {
|
||||
"CONVERSATIONS": {
|
||||
"NAME": "Conversations",
|
||||
"DESC": "( Total )"
|
||||
"NAME": "ውይይቶች",
|
||||
"DESC": "( ጠቅላላ )"
|
||||
},
|
||||
"INCOMING_MESSAGES": {
|
||||
"NAME": "Messages received",
|
||||
"DESC": "( Total )"
|
||||
"DESC": "( ጠቅላላ )"
|
||||
},
|
||||
"OUTGOING_MESSAGES": {
|
||||
"NAME": "Messages sent",
|
||||
"DESC": "( Total )"
|
||||
"DESC": "( ጠቅላላ )"
|
||||
},
|
||||
"FIRST_RESPONSE_TIME": {
|
||||
"NAME": "First Response Time",
|
||||
"DESC": "( Avg )",
|
||||
"DESC": "( አማካይ )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "First Response Time is {metricValue} (based on {conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "Resolution Time",
|
||||
"DESC": "( Avg )",
|
||||
"NAME": "የመፍትሄ ጊዜ",
|
||||
"DESC": "( አማካይ )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "Resolution Time is {metricValue} (based on {conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "Resolution Count",
|
||||
"DESC": "( Total )"
|
||||
"NAME": "የመፍትሄ ብዛት",
|
||||
"DESC": "( ጠቅላላ )"
|
||||
},
|
||||
"BOT_RESOLUTION_COUNT": {
|
||||
"NAME": "Resolution Count",
|
||||
@@ -61,8 +61,8 @@
|
||||
"CUSTOM_DATE_RANGE": "Custom date range"
|
||||
},
|
||||
"CUSTOM_DATE_RANGE": {
|
||||
"CONFIRM": "Apply",
|
||||
"PLACEHOLDER": "Select date range"
|
||||
"CONFIRM": "አተግባር ላይ አውርድ",
|
||||
"PLACEHOLDER": "የቀን ክልል ይምረጡ"
|
||||
},
|
||||
"GROUP_BY_FILTER_DROPDOWN_LABEL": "Group By",
|
||||
"DURATION_FILTER_LABEL": "Duration",
|
||||
@@ -127,12 +127,12 @@
|
||||
}
|
||||
},
|
||||
"AGENT_REPORTS": {
|
||||
"HEADER": "Agents Overview",
|
||||
"HEADER": "የወኪሎች እይታ",
|
||||
"DESCRIPTION": "Easily track agent performance with key metrics such as conversations, response times, resolution times, and resolved cases. Click an agent's name to learn more.",
|
||||
"LOADING_CHART": "Loading chart data...",
|
||||
"NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.",
|
||||
"DOWNLOAD_AGENT_REPORTS": "Download agent reports",
|
||||
"FILTER_DROPDOWN_LABEL": "Select Agent",
|
||||
"LOADING_CHART": "የቅርጸ ቁምፊ ውሂብ በመጫን ላይ...",
|
||||
"NO_ENOUGH_DATA": "ሪፖርት ለማቅረብ በቂ ውሂብ አልደረሰንም፣ እባክዎ በኋላ ደግመው ይሞክሩ።.",
|
||||
"DOWNLOAD_AGENT_REPORTS": "የAgent ሪፖርቶችን አውርድ",
|
||||
"FILTER_DROPDOWN_LABEL": "Agent ይምረጡ",
|
||||
"FILTERS": {
|
||||
"INPUT_PLACEHOLDER": {
|
||||
"AGENTS": "Search agents"
|
||||
@@ -140,72 +140,72 @@
|
||||
},
|
||||
"METRICS": {
|
||||
"CONVERSATIONS": {
|
||||
"NAME": "Conversations",
|
||||
"DESC": "( Total )"
|
||||
"NAME": "ውይይቶች",
|
||||
"DESC": "( ጠቅላላ )"
|
||||
},
|
||||
"INCOMING_MESSAGES": {
|
||||
"NAME": "Incoming Messages",
|
||||
"DESC": "( Total )"
|
||||
"NAME": "የመጣ መልእክቶች",
|
||||
"DESC": "( ጠቅላላ )"
|
||||
},
|
||||
"OUTGOING_MESSAGES": {
|
||||
"NAME": "Outgoing Messages",
|
||||
"DESC": "( Total )"
|
||||
"NAME": "የሚያልኩ መልእክቶች",
|
||||
"DESC": "( ጠቅላላ )"
|
||||
},
|
||||
"FIRST_RESPONSE_TIME": {
|
||||
"NAME": "First Response Time",
|
||||
"DESC": "( Avg )",
|
||||
"DESC": "( አማካይ )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "First Response Time is {metricValue} (based on {conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "Resolution Time",
|
||||
"DESC": "( Avg )",
|
||||
"NAME": "የመፍትሄ ጊዜ",
|
||||
"DESC": "( አማካይ )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "Resolution Time is {metricValue} (based on {conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "Resolution Count",
|
||||
"DESC": "( Total )"
|
||||
"NAME": "የተፈታ ብዛት",
|
||||
"DESC": "( ጠቅላላ )"
|
||||
}
|
||||
},
|
||||
"DATE_RANGE": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "Last 7 days"
|
||||
"name": "ያለፉት 7 ቀናት"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Last 30 days"
|
||||
"name": "ያለፉት 30 ቀናት"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Last 3 months"
|
||||
"name": "ያለፉት 3 ወራት"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Last 6 months"
|
||||
"name": "ያለፉት 6 ወራት"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Last year"
|
||||
"name": "ያለፈው ዓመት"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Custom date range"
|
||||
"name": "በተለይ የተመረጠ ቀን ክልል"
|
||||
}
|
||||
],
|
||||
"CUSTOM_DATE_RANGE": {
|
||||
"CONFIRM": "Apply",
|
||||
"PLACEHOLDER": "Select date range"
|
||||
"CONFIRM": "ተግባሩን ተፈጽም",
|
||||
"PLACEHOLDER": "የቀን ክልል ይምረጡ"
|
||||
}
|
||||
},
|
||||
"LABEL_REPORTS": {
|
||||
"HEADER": "Labels Overview",
|
||||
"HEADER": "የመለያ አጠቃላይ እይታ",
|
||||
"DESCRIPTION": "Track label performance with key metrics including conversations, response times, resolution times, and resolved cases. Click a label name for detailed insights.",
|
||||
"LOADING_CHART": "Loading chart data...",
|
||||
"NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.",
|
||||
"DOWNLOAD_LABEL_REPORTS": "Download label reports",
|
||||
"FILTER_DROPDOWN_LABEL": "Select Label",
|
||||
"LOADING_CHART": "የገበታ ውሂብ በመጫን ላይ...",
|
||||
"NO_ENOUGH_DATA": "የበቂ ውሂብ አልተሰበሰበም፣ እባክዎ በኋላ ይሞክሩ።",
|
||||
"DOWNLOAD_LABEL_REPORTS": "የመለያ ሪፖርቶችን ይውሰዱ",
|
||||
"FILTER_DROPDOWN_LABEL": "መለያ ይምረጡ",
|
||||
"FILTERS": {
|
||||
"INPUT_PLACEHOLDER": {
|
||||
"LABELS": "Search labels"
|
||||
@@ -213,71 +213,71 @@
|
||||
},
|
||||
"METRICS": {
|
||||
"CONVERSATIONS": {
|
||||
"NAME": "Conversations",
|
||||
"DESC": "( Total )"
|
||||
"NAME": "ውይይቶች",
|
||||
"DESC": "( ድምር )"
|
||||
},
|
||||
"INCOMING_MESSAGES": {
|
||||
"NAME": "Incoming Messages",
|
||||
"DESC": "( Total )"
|
||||
"NAME": "የሚገቡ መልእክቶች",
|
||||
"DESC": "( ድምር )"
|
||||
},
|
||||
"OUTGOING_MESSAGES": {
|
||||
"NAME": "Outgoing Messages",
|
||||
"DESC": "( Total )"
|
||||
"NAME": "የወጪ መልእክቶች",
|
||||
"DESC": "( ድምር )"
|
||||
},
|
||||
"FIRST_RESPONSE_TIME": {
|
||||
"NAME": "First Response Time",
|
||||
"DESC": "( Avg )",
|
||||
"DESC": "( አማካይ )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "First Response Time is {metricValue} (based on {conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "Resolution Time",
|
||||
"DESC": "( Avg )",
|
||||
"NAME": "የመፍትሄ ጊዜ",
|
||||
"DESC": "( አማካይ )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "Resolution Time is {metricValue} (based on {conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "Resolution Count",
|
||||
"DESC": "( Total )"
|
||||
"NAME": "የመፍትሄ ብዛት",
|
||||
"DESC": "( ድምር )"
|
||||
}
|
||||
},
|
||||
"DATE_RANGE": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "Last 7 days"
|
||||
"name": "ያለፉ 7 ቀናት"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Last 30 days"
|
||||
"name": "ያለፉ 30 ቀናት"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Last 3 months"
|
||||
"name": "ያለፉት 3 ወራት"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Last 6 months"
|
||||
"name": "ያለፉት 6 ወራት"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Last year"
|
||||
"name": "ያለፈው ዓመት"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Custom date range"
|
||||
"name": "በተፈጥሮ የተመረጠ የቀን ክልል"
|
||||
}
|
||||
],
|
||||
"CUSTOM_DATE_RANGE": {
|
||||
"CONFIRM": "Apply",
|
||||
"PLACEHOLDER": "Select date range"
|
||||
"CONFIRM": "ተግባር አድርግ",
|
||||
"PLACEHOLDER": "የቀን ክልል ይምረጡ"
|
||||
}
|
||||
},
|
||||
"INBOX_REPORTS": {
|
||||
"HEADER": "Inbox Overview",
|
||||
"HEADER": "የኢንቦክስ አጠቃላይ እይታ",
|
||||
"DESCRIPTION": "Quickly view your inbox performance with key metrics like conversations, response times, resolution times, and resolved cases—all in one place. Click an inbox name for more details.",
|
||||
"LOADING_CHART": "Loading chart data...",
|
||||
"NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.",
|
||||
"DOWNLOAD_INBOX_REPORTS": "Download inbox reports",
|
||||
"LOADING_CHART": "የገቢ ግምገማ መረጃ በመጫን ላይ...",
|
||||
"NO_ENOUGH_DATA": "ሪፖርት ለማዘጋጀት በቂ መረጃ አልተደረሰም። እባክዎ በኋላ ይሞክሩ።",
|
||||
"DOWNLOAD_INBOX_REPORTS": "የኢንቦክስ ሪፖርቶችን ይውሰዱ",
|
||||
"FILTER_DROPDOWN_LABEL": "Select Inbox",
|
||||
"ALL_INBOXES": "All Inboxes",
|
||||
"SEARCH_INBOX": "Search Inbox",
|
||||
@@ -424,7 +424,7 @@
|
||||
}
|
||||
},
|
||||
"CSAT_REPORTS": {
|
||||
"HEADER": "CSAT Reports",
|
||||
"HEADER": "CSAT ሪፖርቶች",
|
||||
"NO_RECORDS": "No responses yet",
|
||||
"NO_RECORDS_DESCRIPTION": "CSAT survey responses will appear here once customers start providing feedback.",
|
||||
"DOWNLOAD": "Download CSAT Reports",
|
||||
@@ -454,10 +454,10 @@
|
||||
},
|
||||
"TABLE": {
|
||||
"HEADER": {
|
||||
"CONTACT_NAME": "Contact",
|
||||
"CONTACT_NAME": "እውቂያ",
|
||||
"AGENT_NAME": "Agent",
|
||||
"RATING": "Rating",
|
||||
"FEEDBACK_TEXT": "Feedback comment",
|
||||
"RATING": "እምነት ደረጃ",
|
||||
"FEEDBACK_TEXT": "አስተያየት አስተያየት",
|
||||
"CONVERSATION": "Conversation",
|
||||
"CUSTOMER": "Customer",
|
||||
"RESPONSE": "Response",
|
||||
@@ -469,16 +469,16 @@
|
||||
"NO_FEEDBACK": "No feedback provided",
|
||||
"METRIC": {
|
||||
"TOTAL_RESPONSES": {
|
||||
"LABEL": "Total responses",
|
||||
"TOOLTIP": "Total number of responses collected"
|
||||
"LABEL": "ጠቅላላ ምላሾች",
|
||||
"TOOLTIP": "የተሰበሰበው ምላሾች ጠቅላላ ብዛት"
|
||||
},
|
||||
"SATISFACTION_SCORE": {
|
||||
"LABEL": "Satisfaction score",
|
||||
"TOOLTIP": "Total number of positive responses / Total number of responses * 100"
|
||||
"LABEL": "የደስታ ነጥብ",
|
||||
"TOOLTIP": "አጠቃላይ የአዎንታዊ ምላሾች ብዛት / አጠቃላይ የምላሾች ብዛት * 100"
|
||||
},
|
||||
"RESPONSE_RATE": {
|
||||
"LABEL": "Response rate",
|
||||
"TOOLTIP": "Total number of responses / Total number of CSAT survey messages sent * 100"
|
||||
"LABEL": "የምላሽ ተመን",
|
||||
"TOOLTIP": "አጠቃላይ የምላሾች ብዛት / አጠቃላይ የተላኩ የCSAT እቅድ መልእክቶች ብዛት * 100"
|
||||
},
|
||||
"RATING_DISTRIBUTION": "Rating distribution"
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -316,6 +316,18 @@
|
||||
"SUCCESS_MESSAGE": "تم إزالة اللغة من البوابة بنجاح",
|
||||
"ERROR_MESSAGE": "غير قادر على إزالة اللغة من البوابة. حاول مرة أخرى."
|
||||
}
|
||||
},
|
||||
"DRAFT_LOCALE": {
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Locale moved to draft successfully",
|
||||
"ERROR_MESSAGE": "Unable to move locale to draft. Try again."
|
||||
}
|
||||
},
|
||||
"PUBLISH_LOCALE": {
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Locale published successfully",
|
||||
"ERROR_MESSAGE": "Unable to publish locale. Try again."
|
||||
}
|
||||
}
|
||||
},
|
||||
"TABLE": {
|
||||
@@ -644,8 +656,11 @@
|
||||
"ARTICLES_COUNT": "{count} article | {count} articles",
|
||||
"CATEGORIES_COUNT": "{count} category | {count} categories",
|
||||
"DEFAULT": "افتراضي",
|
||||
"DRAFT": "مسودة",
|
||||
"DROPDOWN_MENU": {
|
||||
"MAKE_DEFAULT": "Make default",
|
||||
"MOVE_TO_DRAFT": "Move to draft",
|
||||
"PUBLISH_LOCALE": "Publish locale",
|
||||
"DELETE": "حذف"
|
||||
}
|
||||
},
|
||||
@@ -655,6 +670,13 @@
|
||||
"COMBOBOX": {
|
||||
"PLACEHOLDER": "حدد اللغة..."
|
||||
},
|
||||
"STATUS": {
|
||||
"LABEL": "الحالة",
|
||||
"OPTIONS": {
|
||||
"LIVE": "نُشرت",
|
||||
"DRAFT": "مسودة"
|
||||
}
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "تمت إضافة اللغة بنجاح",
|
||||
"ERROR_MESSAGE": "غير قادر على إضافة اللغة . حاول مرة أخرى."
|
||||
|
||||
@@ -710,9 +710,21 @@
|
||||
"MESSENGER_SUB_HEAD": "ضع هذا الكود داخل وسم الـ body في موقعك",
|
||||
"ALLOWED_DOMAINS": {
|
||||
"TITLE": "Allowed Domains",
|
||||
"SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.",
|
||||
"DESCRIPTION": "Restrict which websites can embed your chat widget. For security, only add domains you own and trust. Add one or more domains separated by commas. Leave blank to allow all domains (not recommended for production).",
|
||||
"PLACEHOLDER": "example.com, www.example.com, app.example.com"
|
||||
},
|
||||
"ALLOW_MOBILE_WEBVIEW": {
|
||||
"LABEL": "Enable widget in mobile apps",
|
||||
"SUBTITLE": "حدد هذا الخيار إذا كنت تقوم بتضمين الأداة في تطبيقات iOS أو Android. لا ترسل تطبيقات الجوال معلومات النطاق، لذا سيتم حظرها بسبب قيود النطاق ما لم يتم تفعيل هذا الخيار."
|
||||
},
|
||||
"IDENTITY_VALIDATION": {
|
||||
"TITLE": "Identity Validation",
|
||||
"DESCRIPTION": "Verify user authenticity by generating secure tokens. This prevents unauthorized users from impersonating others in your chat.",
|
||||
"SECRET_KEY": "Secret Key",
|
||||
"VIEW_DOCS": "View documentation",
|
||||
"REQUIRE_LABEL": "Require identity validation for all conversations",
|
||||
"REQUIRE_DESCRIPTION": "When enabled, users must provide a valid identity token to start conversations. Requests without valid tokens will be rejected."
|
||||
},
|
||||
"INBOX_AGENTS": "وكيل الدعم",
|
||||
"INBOX_AGENTS_SUB_TEXT": "إضافة أو إزالة وكلاء من صندوق الوارد هذا",
|
||||
"AGENT_ASSIGNMENT": "تعيين المحادثة",
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
},
|
||||
"HELP_TEXT": {
|
||||
"TITLE": "استخدام تكامل Slack",
|
||||
"BODY": "With this integration, all of your incoming conversations will be synced to the ***{selectedChannelName}*** channel in your Slack workspace. You can manage all your customer conversations right within the channel and never miss a message.\n\nHere are the main features of the integration:\n\n**Respond to conversations from within Slack:** To respond to a conversation in the ***{selectedChannelName}*** Slack channel, simply type out your message and send it as a thread. This will create a response back to the customer through Chatwoot. It's that simple!\n\n **Create private notes:** If you want to create private notes instead of replies, start your message with ***`note:`***. This ensures that your message is kept private and won't be visible to the customer.\n\n**Associate an agent profile:** If the person who replied on Slack has an agent profile in Chatwoot under the same email, the replies will be associated with that agent profile automatically. This means you can easily track who said what and when. On the other hand, when the replier doesn't have an associated agent profile, the replies will appear from the bot profile to the customer.",
|
||||
"BODY": "باستخدام هذا التكامل، ستتم مزامنة جميع محادثاتك الواردة مع قناة ***{selectedChannelName}*** في مساحة عمل Slack الخاصة بك. يمكنك إدارة جميع محادثات عملائك مباشرة من داخل القناة ولن تفوّت أي رسالة.\n\nفيما يلي الميزات الرئيسية لهذا التكامل:\n\n**الرد على المحادثات من داخل Slack:** للرد على محادثة في قناة Slack ***{selectedChannelName}***، ما عليك سوى كتابة رسالتك وإرسالها كسلسلة رسائل. سيؤدي ذلك إلى إنشاء رد للعميل عبر Chatwoot. الأمر بهذه البساطة!\n\n **إنشاء ملاحظات خاصة:** إذا كنت تريد إنشاء ملاحظات خاصة بدلاً من الردود، فابدأ رسالتك بـ ***`note:`***. يضمن ذلك بقاء رسالتك خاصة وعدم ظهورها للعميل.\n\n**ربط ملف وكيل:** إذا كان الشخص الذي ردّ في Slack لديه ملف وكيل في Chatwoot تحت البريد الإلكتروني نفسه، فسيتم ربط الردود تلقائيًا بذلك الملف. وهذا يعني أنه يمكنك بسهولة تتبع من قال ماذا ومتى. من ناحية أخرى، إذا لم يكن لدى الشخص الذي ردّ ملف وكيل مرتبط، فستظهر الردود للعميل على أنها صادرة من ملف البوت.",
|
||||
"SELECTED": "selected"
|
||||
},
|
||||
"SELECT_CHANNEL": {
|
||||
@@ -390,72 +390,72 @@
|
||||
},
|
||||
"CAPTAIN": {
|
||||
"NAME": "قائد",
|
||||
"HEADER_KNOW_MORE": "Know more",
|
||||
"HEADER_KNOW_MORE": "اعرف المزيد",
|
||||
"ASSISTANT_SWITCHER": {
|
||||
"ASSISTANTS": "Assistants",
|
||||
"SWITCH_ASSISTANT": "Switch between assistants",
|
||||
"NEW_ASSISTANT": "Create Assistant",
|
||||
"EMPTY_LIST": "No assistants found, please create one to get started"
|
||||
"ASSISTANTS": "المساعدون",
|
||||
"SWITCH_ASSISTANT": "التبديل بين المساعدين",
|
||||
"NEW_ASSISTANT": "إنشاء مساعد",
|
||||
"EMPTY_LIST": "لم يتم العثور على مساعدين، يرجى إنشاء واحد للبدء"
|
||||
},
|
||||
"COPILOT": {
|
||||
"TITLE": "Copilot",
|
||||
"TRY_THESE_PROMPTS": "Try these prompts",
|
||||
"PANEL_TITLE": "Get started with Copilot",
|
||||
"KICK_OFF_MESSAGE": "Need a quick summary, want to check past conversations, or draft a better reply? Copilot’s here to speed things up.",
|
||||
"PANEL_TITLE": "ابدأ مع Copilot",
|
||||
"KICK_OFF_MESSAGE": "هل تحتاج إلى ملخص سريع، ترغب في مراجعة المحادثات السابقة، أو صياغة رد أفضل؟ Copilot هنا لتسريع الأمور.",
|
||||
"SEND_MESSAGE": "إرسال الرسالة...",
|
||||
"EMPTY_MESSAGE": "There was an error generating the response. Please try again.",
|
||||
"LOADER": "Captain is thinking",
|
||||
"EMPTY_MESSAGE": "حدث خطأ أثناء توليد الاستجابة. يرجى المحاولة مرة أخرى.",
|
||||
"LOADER": "يقوم Captain بالتفكير",
|
||||
"YOU": "أنت",
|
||||
"USE": "Use this",
|
||||
"RESET": "Reset",
|
||||
"SHOW_STEPS": "Show steps",
|
||||
"SELECT_ASSISTANT": "Select Assistant",
|
||||
"USE": "استخدام هذا",
|
||||
"RESET": "إعادة تعيين",
|
||||
"SHOW_STEPS": "عرض الخطوات",
|
||||
"SELECT_ASSISTANT": "اختر المساعد",
|
||||
"PROMPTS": {
|
||||
"SUMMARIZE": {
|
||||
"LABEL": "Summarize this conversation",
|
||||
"CONTENT": "Summarize the key points discussed between the customer and the support agent, including the customer's concerns, questions, and the solutions or responses provided by the support agent"
|
||||
"LABEL": "لخص هذه المحادثة",
|
||||
"CONTENT": "لخص النقاط الرئيسية التي نوقشت بين العميل ووكيل الدعم، بما في ذلك مخاوف العميل، أسئلته، والحلول أو الردود المقدمة من الوكيل."
|
||||
},
|
||||
"SUGGEST": {
|
||||
"LABEL": "Suggest an answer",
|
||||
"CONTENT": "Analyze the customer's inquiry, and draft a response that effectively addresses their concerns or questions. Ensure the reply is clear, concise, and provides helpful information."
|
||||
"LABEL": "اقترح إجابة",
|
||||
"CONTENT": "حلل استفسار العميل وقم بصياغة رد يعالج مخاوفه أو أسئلته بفعالية. تأكد من أن الرد واضح، موجز، ويوفر معلومات مفيدة."
|
||||
},
|
||||
"RATE": {
|
||||
"LABEL": "Rate this conversation",
|
||||
"CONTENT": "Review the conversation to see how well it meets the customer's needs. Share a rating out of 5 based on tone, clarity, and effectiveness."
|
||||
"LABEL": "قم بتقييم هذه المحادثة",
|
||||
"CONTENT": "راجع المحادثة لترى مدى تلبيتها لاحتياجات العميل. شارك تقييمًا من 5 بناءً على النغمة، الوضوح، والفعالية."
|
||||
},
|
||||
"HIGH_PRIORITY": {
|
||||
"LABEL": "High priority conversations",
|
||||
"CONTENT": "Give me a summary of all high priority open conversations. Include the conversation ID, customer name (if available), last message content, and assigned agent. Group by status if relevant."
|
||||
"LABEL": "المحادثات ذات الأولوية العالية",
|
||||
"CONTENT": "اعطني ملخصًا لجميع المحادثات المفتوحة ذات الأولوية العالية. تضمّن معرف المحادثة، اسم العميل (إن وُجد)، محتوى آخر رسالة، والوكيل المعين. قم بالتجميع حسب الحالة إذا كان ذلك مناسبًا."
|
||||
},
|
||||
"LIST_CONTACTS": {
|
||||
"LABEL": "List contacts",
|
||||
"CONTENT": "Show me the list of top 10 contacts. Include name, email or phone number (if available), last seen time, tags (if any)."
|
||||
"LABEL": "قائمة جهات الاتصال",
|
||||
"CONTENT": "اعرض لي قائمة بأفضل 10 جهات اتصال. تضمّن الاسم، البريد الإلكتروني أو رقم الهاتف (إن وُجد)، آخر وقت مشاهدة، العلامات (إن وجدت)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"PLAYGROUND": {
|
||||
"USER": "أنت",
|
||||
"ASSISTANT": "Assistant",
|
||||
"ASSISTANT": "مساعد",
|
||||
"MESSAGE_PLACEHOLDER": "أكتب رسالتك...",
|
||||
"HEADER": "Playground",
|
||||
"DESCRIPTION": "Use this playground to send messages to your assistant and check if it responds accurately, quickly, and in the tone you expect.",
|
||||
"CREDIT_NOTE": "Messages sent here will count toward your Captain credits."
|
||||
"HEADER": "ساحة اللعب",
|
||||
"DESCRIPTION": "استخدم هذه الساحة لإرسال رسائل إلى مساعدك والتحقق مما إذا كان يرد بدقة وسرعة وبالنغمة التي تتوقعها.",
|
||||
"CREDIT_NOTE": "الرسائل المرسلة هنا ستُحتسب ضمن رصيد Captain الخاص بك."
|
||||
},
|
||||
"PAYWALL": {
|
||||
"TITLE": "Upgrade to use Captain AI",
|
||||
"AVAILABLE_ON": "Captain is not available on the free plan.",
|
||||
"UPGRADE_PROMPT": "Upgrade your plan to get access to our assistants, copilot and more.",
|
||||
"TITLE": "قم بالترقية لاستخدام Captain AI",
|
||||
"AVAILABLE_ON": "Captain غير متاح على الخطة المجانية.",
|
||||
"UPGRADE_PROMPT": "قم بترقية خطتك للحصول على الوصول إلى مساعدينا، وcopilot، والمزيد.",
|
||||
"UPGRADE_NOW": "Upgrade now",
|
||||
"CANCEL_ANYTIME": "You can change or cancel your plan anytime"
|
||||
},
|
||||
"ENTERPRISE_PAYWALL": {
|
||||
"AVAILABLE_ON": "ولا يتوفر الكابتن AI إلا في خطط المؤسسة.",
|
||||
"UPGRADE_PROMPT": "Upgrade your plan to get access to our assistants, copilot and more.",
|
||||
"UPGRADE_PROMPT": "قم بترقية خطتك للحصول على الوصول إلى مساعدينا، وcopilot، والمزيد.",
|
||||
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
|
||||
},
|
||||
"BANNER": {
|
||||
"RESPONSES": "You've used over 80% of your response limit. To continue using Captain AI, please upgrade.",
|
||||
"DOCUMENTS": "Document limit reached. Upgrade to continue using Captain AI."
|
||||
"RESPONSES": "لقد استخدمت أكثر من 80٪ من حد الاستجابات الخاص بك. للاستمرار في استخدام Captain AI، يرجى الترقية.",
|
||||
"DOCUMENTS": "تم الوصول إلى حد المستندات. قم بالترقية للاستمرار في استخدام Captain AI."
|
||||
},
|
||||
"FORM": {
|
||||
"CANCEL": "إلغاء",
|
||||
@@ -527,7 +527,8 @@
|
||||
"TITLE": "الخصائص",
|
||||
"ALLOW_CONVERSATION_FAQS": "Generate FAQs from resolved conversations",
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions.",
|
||||
"ALLOW_CITATIONS": "Include source citations in responses"
|
||||
"ALLOW_CITATIONS": "Include source citations in responses",
|
||||
"ALLOW_CONTACT_ATTRIBUTES": "Allow access to contact information"
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
@@ -737,6 +738,17 @@
|
||||
"DOCUMENTS": {
|
||||
"HEADER": "Documents",
|
||||
"ADD_NEW": "Create a new document",
|
||||
"SELECTED": "{count} selected",
|
||||
"SELECT_ALL": "Select all ({count})",
|
||||
"UNSELECT_ALL": "Unselect all ({count})",
|
||||
"BULK_DELETE_BUTTON": "حذف",
|
||||
"BULK_DELETE": {
|
||||
"TITLE": "Delete documents?",
|
||||
"DESCRIPTION": "Are you sure you want to delete the selected documents? This action cannot be undone.",
|
||||
"CONFIRM": "Yes, delete all",
|
||||
"SUCCESS_MESSAGE": "Documents deleted successfully",
|
||||
"ERROR_MESSAGE": "There was an error deleting the documents, please try again."
|
||||
},
|
||||
"RELATED_RESPONSES": {
|
||||
"TITLE": "Related FAQs",
|
||||
"DESCRIPTION": "These FAQs are generated directly from the document."
|
||||
@@ -794,6 +806,7 @@
|
||||
"CUSTOM_TOOLS": {
|
||||
"HEADER": "Tools",
|
||||
"ADD_NEW": "Create a new tool",
|
||||
"SOFT_LIMIT_WARNING": "Having more than 10 tools may reduce the assistant's reliability in selecting the right tool. Consider removing unused tools for better results.",
|
||||
"EMPTY_STATE": {
|
||||
"TITLE": "No custom tools available",
|
||||
"SUBTITLE": "Create custom tools to connect your assistant with external APIs and services, enabling it to fetch data and perform actions on your behalf.",
|
||||
@@ -824,11 +837,18 @@
|
||||
"SUCCESS_MESSAGE": "Custom tool deleted successfully",
|
||||
"ERROR_MESSAGE": "Failed to delete custom tool"
|
||||
},
|
||||
"TEST": {
|
||||
"BUTTON": "Test connection",
|
||||
"SUCCESS": "Endpoint returned HTTP {status}",
|
||||
"ERROR": "Connection failed",
|
||||
"DISABLED_HINT": "Testing is only available for endpoints without templates or request bodies."
|
||||
},
|
||||
"FORM": {
|
||||
"TITLE": {
|
||||
"LABEL": "Tool Name",
|
||||
"PLACEHOLDER": "Order Lookup",
|
||||
"ERROR": "Tool name is required"
|
||||
"ERROR": "Tool name is required",
|
||||
"MAX_LENGTH_ERROR": "Tool name must be {max} characters or fewer"
|
||||
},
|
||||
"DESCRIPTION": {
|
||||
"LABEL": "الوصف",
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
{
|
||||
"CONTACT_PANEL": {
|
||||
"NOT_AVAILABLE": "Not Available",
|
||||
"NOT_AVAILABLE": "Mövcud deyil",
|
||||
"EMAIL_ADDRESS": "Email Address",
|
||||
"PHONE_NUMBER": "Phone number",
|
||||
"PHONE_NUMBER": "Telefon nömrəsi",
|
||||
"IDENTIFIER": "Identifier",
|
||||
"COPY_SUCCESSFUL": "Copied to clipboard successfully",
|
||||
"COMPANY": "Company",
|
||||
"LOCATION": "Location",
|
||||
"COMPANY": "Şirkət",
|
||||
"LOCATION": "Yer",
|
||||
"BROWSER_LANGUAGE": "Browser Language",
|
||||
"CONVERSATION_TITLE": "Conversation Details",
|
||||
"VIEW_PROFILE": "View Profile",
|
||||
"BROWSER": "Browser",
|
||||
"OS": "Operating System",
|
||||
"INITIATED_FROM": "Initiated from",
|
||||
"INITIATED_AT": "Initiated at",
|
||||
"IP_ADDRESS": "IP Address",
|
||||
"CREATED_AT_LABEL": "Created",
|
||||
"OS": "Əməliyyat Sistemi",
|
||||
"INITIATED_FROM": "Başlanğıc yeri",
|
||||
"INITIATED_AT": "Başlanğıc vaxtı",
|
||||
"IP_ADDRESS": "IP ünvanı",
|
||||
"CREATED_AT_LABEL": "Yaradılıb",
|
||||
"NEW_MESSAGE": "New message",
|
||||
"CALL": "Call",
|
||||
"CALL": "Zəng et",
|
||||
"CALL_INITIATED": "Calling the contact…",
|
||||
"CALL_FAILED": "Unable to start the call. Please try again.",
|
||||
"VOICE_INBOX_PICKER": {
|
||||
"TITLE": "Choose a voice inbox"
|
||||
},
|
||||
"CONVERSATIONS": {
|
||||
"NO_RECORDS_FOUND": "There are no previous conversations associated to this contact.",
|
||||
"TITLE": "Previous Conversations"
|
||||
"NO_RECORDS_FOUND": "Bu əlaqə ilə bağlı əvvəlki söhbətlər yoxdur.",
|
||||
"TITLE": "Əvvəlki Söhbətlər"
|
||||
},
|
||||
"LABELS": {
|
||||
"CONTACT": {
|
||||
@@ -44,23 +44,23 @@
|
||||
}
|
||||
},
|
||||
"MERGE_CONTACT": "Merge contact",
|
||||
"CONTACT_ACTIONS": "Contact actions",
|
||||
"CONTACT_ACTIONS": "Əlaqə əməliyyatları",
|
||||
"MUTE_CONTACT": "Block Contact",
|
||||
"UNMUTE_CONTACT": "Unblock Contact",
|
||||
"MUTED_SUCCESS": "This contact is blocked successfully. You will not be notified of any future conversations.",
|
||||
"UNMUTED_SUCCESS": "This contact is unblocked successfully.",
|
||||
"SEND_TRANSCRIPT": "Send Transcript",
|
||||
"EDIT_LABEL": "Edit",
|
||||
"UNMUTED_SUCCESS": "Bu əlaqənin bloku uğurla açıldı.",
|
||||
"SEND_TRANSCRIPT": "Mətni Göndər",
|
||||
"EDIT_LABEL": "Redaktə et",
|
||||
"SIDEBAR_SECTIONS": {
|
||||
"CUSTOM_ATTRIBUTES": "Custom Attributes",
|
||||
"CONTACT_LABELS": "Contact Labels",
|
||||
"PREVIOUS_CONVERSATIONS": "Previous Conversations",
|
||||
"PREVIOUS_CONVERSATIONS": "Əvvəlki Söhbətlər",
|
||||
"NO_RECORDS_FOUND": "No attributes found"
|
||||
}
|
||||
},
|
||||
"EDIT_CONTACT": {
|
||||
"BUTTON_LABEL": "Edit Contact",
|
||||
"TITLE": "Edit contact",
|
||||
"BUTTON_LABEL": "Əlaqəni redaktə et",
|
||||
"TITLE": "Əlaqəni redaktə et",
|
||||
"DESC": "Edit contact details"
|
||||
},
|
||||
"DELETE_CONTACT": {
|
||||
@@ -71,23 +71,23 @@
|
||||
"TITLE": "Confirm Deletion",
|
||||
"MESSAGE": "Are you sure to delete ",
|
||||
"YES": "Yes, Delete",
|
||||
"NO": "No, Keep"
|
||||
"NO": "Xeyr, Saxla"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Contact deleted successfully",
|
||||
"ERROR_MESSAGE": "Could not delete contact. Please try again later."
|
||||
"ERROR_MESSAGE": "Əlaqəni silmək mümkün olmadı. Zəhmət olmasa, bir az sonra yenidən cəhd edin."
|
||||
}
|
||||
},
|
||||
"CONTACT_FORM": {
|
||||
"FORM": {
|
||||
"SUBMIT": "Submit",
|
||||
"CANCEL": "Cancel",
|
||||
"CANCEL": "Ləğv et",
|
||||
"AVATAR": {
|
||||
"LABEL": "Contact Avatar"
|
||||
},
|
||||
"NAME": {
|
||||
"PLACEHOLDER": "Enter the full name of the contact",
|
||||
"LABEL": "Full Name"
|
||||
"PLACEHOLDER": "Əlaqənin tam adını daxil edin",
|
||||
"LABEL": "Tam Ad"
|
||||
},
|
||||
"BIO": {
|
||||
"PLACEHOLDER": "Enter the bio of the contact",
|
||||
@@ -100,30 +100,30 @@
|
||||
"ERROR": "Please enter a valid email address."
|
||||
},
|
||||
"PHONE_NUMBER": {
|
||||
"PLACEHOLDER": "Enter the phone number of the contact",
|
||||
"LABEL": "Phone Number",
|
||||
"HELP": "Phone number should be of E.164 format eg: +1415555555 [+][country code][area code][local phone number]. You can select the dial code from the dropdown.",
|
||||
"ERROR": "Phone number should be either empty or of E.164 format",
|
||||
"DIAL_CODE_ERROR": "Please select a dial code from the list",
|
||||
"DUPLICATE": "This phone number is in use for another contact."
|
||||
"PLACEHOLDER": "Əlaqə şəxsin telefon nömrəsini daxil edin",
|
||||
"LABEL": "Telefon nömrəsi",
|
||||
"HELP": "Telefon nömrəsi E.164 formatında olmalıdır, məsələn: +1415555555. Ölkə kodunu siyahıdan seçə bilərsiniz.",
|
||||
"ERROR": "Telefon nömrəsi ya boş olmalıdır, ya da E.164 formatında olmalıdır",
|
||||
"DIAL_CODE_ERROR": "Zəhmət olmasa siyahıdan kodu seçin",
|
||||
"DUPLICATE": "Bu telefon nömrəsi başqa bir əlaqə üçün istifadə olunur."
|
||||
},
|
||||
"LOCATION": {
|
||||
"PLACEHOLDER": "Enter the location of the contact",
|
||||
"LABEL": "Location"
|
||||
"PLACEHOLDER": "Əlaqə yerini daxil edin",
|
||||
"LABEL": "Yer"
|
||||
},
|
||||
"COMPANY_NAME": {
|
||||
"PLACEHOLDER": "Enter the company name",
|
||||
"PLACEHOLDER": "Şirkət adını daxil edin",
|
||||
"LABEL": "Company Name"
|
||||
},
|
||||
"COUNTRY": {
|
||||
"PLACEHOLDER": "Enter the country name",
|
||||
"PLACEHOLDER": "Ölkə adını daxil edin",
|
||||
"LABEL": "Country Name",
|
||||
"SELECT_PLACEHOLDER": "Select",
|
||||
"REMOVE": "Remove",
|
||||
"SELECT_COUNTRY": "Select Country"
|
||||
"SELECT_PLACEHOLDER": "Seçin",
|
||||
"REMOVE": "Sil",
|
||||
"SELECT_COUNTRY": "Ölkəni seçin"
|
||||
},
|
||||
"CITY": {
|
||||
"PLACEHOLDER": "Enter the city name",
|
||||
"PLACEHOLDER": "Şəhərin adını daxil edin",
|
||||
"LABEL": "City Name"
|
||||
},
|
||||
"SOCIAL_PROFILES": {
|
||||
@@ -155,23 +155,23 @@
|
||||
"ERROR_MESSAGE": "There was an error, please try again"
|
||||
},
|
||||
"NEW_CONVERSATION": {
|
||||
"BUTTON_LABEL": "Start conversation",
|
||||
"TITLE": "New conversation",
|
||||
"BUTTON_LABEL": "Söhbətə başla",
|
||||
"TITLE": "Yeni söhbət",
|
||||
"DESC": "Start a new conversation by sending a new message.",
|
||||
"NO_INBOX": "Couldn't find an inbox to initiate a new conversation with this contact.",
|
||||
"FORM": {
|
||||
"TO": {
|
||||
"LABEL": "To"
|
||||
"LABEL": "Kimə"
|
||||
},
|
||||
"INBOX": {
|
||||
"LABEL": "Via Inbox",
|
||||
"PLACEHOLDER": "Choose source inbox",
|
||||
"ERROR": "Select an inbox"
|
||||
"ERROR": "Bir qutu seçin"
|
||||
},
|
||||
"SUBJECT": {
|
||||
"LABEL": "Subject",
|
||||
"PLACEHOLDER": "Subject",
|
||||
"ERROR": "Subject can't be empty"
|
||||
"LABEL": "Mövzu",
|
||||
"PLACEHOLDER": "Mövzu",
|
||||
"ERROR": "Mövzu boş ola bilməz"
|
||||
},
|
||||
"MESSAGE": {
|
||||
"LABEL": "Message",
|
||||
@@ -183,10 +183,10 @@
|
||||
"HELP_TEXT": "Drag and drop files here or choose files to attach"
|
||||
},
|
||||
"SUBMIT": "Send message",
|
||||
"CANCEL": "Cancel",
|
||||
"CANCEL": "Ləğv et",
|
||||
"SUCCESS_MESSAGE": "Message sent!",
|
||||
"GO_TO_CONVERSATION": "View",
|
||||
"ERROR_MESSAGE": "Couldn't send! try again"
|
||||
"GO_TO_CONVERSATION": "Bax",
|
||||
"ERROR_MESSAGE": "Göndərmək mümkün olmadı! yenidən cəhd et"
|
||||
}
|
||||
},
|
||||
"CONTACTS_PAGE": {
|
||||
@@ -204,7 +204,7 @@
|
||||
"ACTIONS": {
|
||||
"COPY": "Copy attribute",
|
||||
"DELETE": "Delete attribute",
|
||||
"EDIT": "Edit attribute"
|
||||
"EDIT": "Xüsusiyyəti redaktə et"
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Create custom attribute",
|
||||
@@ -212,7 +212,7 @@
|
||||
},
|
||||
"FORM": {
|
||||
"CREATE": "Add attribute",
|
||||
"CANCEL": "Cancel",
|
||||
"CANCEL": "Ləğv et",
|
||||
"NAME": {
|
||||
"LABEL": "Custom attribute name",
|
||||
"PLACEHOLDER": "Eg: shopify id",
|
||||
@@ -220,12 +220,12 @@
|
||||
},
|
||||
"VALUE": {
|
||||
"LABEL": "Attribute value",
|
||||
"PLACEHOLDER": "Eg: 11901 "
|
||||
"PLACEHOLDER": "Məsələn: 11901 "
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Create new attribute ",
|
||||
"SUCCESS": "Attribute added successfully",
|
||||
"ERROR": "Unable to add attribute. Please try again later"
|
||||
"SUCCESS": "Xüsusiyyət uğurla əlavə edildi",
|
||||
"ERROR": "Xüsusiyyəti əlavə etmək mümkün olmadı. Zəhmət olmasa, bir az sonra yenidən cəhd edin"
|
||||
},
|
||||
"UPDATE": {
|
||||
"SUCCESS": "Attribute updated successfully",
|
||||
@@ -236,52 +236,52 @@
|
||||
"ERROR": "Unable to delete attribute. Please try again later"
|
||||
},
|
||||
"ATTRIBUTE_SELECT": {
|
||||
"TITLE": "Add attributes",
|
||||
"TITLE": "Xüsusiyyətlər əlavə et",
|
||||
"PLACEHOLDER": "Search attributes",
|
||||
"NO_RESULT": "No attributes found"
|
||||
},
|
||||
"ATTRIBUTE_TYPE": {
|
||||
"LIST": {
|
||||
"PLACEHOLDER": "Select value",
|
||||
"PLACEHOLDER": "Dəyəri seçin",
|
||||
"SEARCH_INPUT_PLACEHOLDER": "Search value",
|
||||
"NO_RESULT": "No result found"
|
||||
}
|
||||
}
|
||||
},
|
||||
"VALIDATIONS": {
|
||||
"REQUIRED": "Valid value is required",
|
||||
"INVALID_URL": "Invalid URL",
|
||||
"INVALID_INPUT": "Invalid Input"
|
||||
"REQUIRED": "Düzgün dəyər tələb olunur",
|
||||
"INVALID_URL": "Yanlış URL",
|
||||
"INVALID_INPUT": "Yanlış Giriş"
|
||||
}
|
||||
},
|
||||
"MERGE_CONTACTS": {
|
||||
"TITLE": "Merge contacts",
|
||||
"DESCRIPTION": "Merge contacts to combine two profiles into one, including all attributes and conversations. In case of conflict, the Primary contact’s attributes will take precedence.",
|
||||
"DESCRIPTION": "İki profili bütün atributlar və söhbətlər daxil olmaqla birləşdirərək əlaqələri birləşdirin. Ziddiyyət olduqda, Əsas əlaqənin atributları üstünlük təşkil edəcək.",
|
||||
"PRIMARY": {
|
||||
"TITLE": "Primary contact",
|
||||
"TITLE": "Əsas əlaqə",
|
||||
"HELP_LABEL": "To be deleted"
|
||||
},
|
||||
"PARENT": {
|
||||
"TITLE": "Contact to merge",
|
||||
"PLACEHOLDER": "Search for a contact",
|
||||
"PLACEHOLDER": "Əlaqə axtar",
|
||||
"HELP_LABEL": "To be kept"
|
||||
},
|
||||
"SUMMARY": {
|
||||
"TITLE": "Summary",
|
||||
"TITLE": "Yekun",
|
||||
"DELETE_WARNING": "Contact of <strong>{primaryContactName}</strong> will be deleted.",
|
||||
"ATTRIBUTE_WARNING": "Contact details of <strong>{primaryContactName}</strong> will be copied to <strong>{parentContactName}</strong>."
|
||||
},
|
||||
"SEARCH": {
|
||||
"ERROR_MESSAGE": "Something went wrong. Please try again later."
|
||||
"ERROR_MESSAGE": "Nəsə səhv getdi. Zəhmət olmasa, bir az sonra yenidən cəhd edin."
|
||||
},
|
||||
"FORM": {
|
||||
"SUBMIT": " Merge contacts",
|
||||
"CANCEL": "Cancel",
|
||||
"CANCEL": "Ləğv et",
|
||||
"CHILD_CONTACT": {
|
||||
"ERROR": "Select a child contact to merge"
|
||||
"ERROR": "Birləşdirmək üçün alt əlaqəni seçin"
|
||||
},
|
||||
"SUCCESS_MESSAGE": "Contact merged successfully",
|
||||
"ERROR_MESSAGE": "Could not merge contacts, try again!"
|
||||
"ERROR_MESSAGE": "Əlaqələri birləşdirmək mümkün olmadı, yenidən cəhd edin!"
|
||||
},
|
||||
"DROPDOWN_ITEM": {
|
||||
"ID": "(ID: {identifier})"
|
||||
@@ -289,68 +289,68 @@
|
||||
},
|
||||
"CONTACTS_LAYOUT": {
|
||||
"HEADER": {
|
||||
"TITLE": "Contacts",
|
||||
"SEARCH_TITLE": "Search contacts",
|
||||
"ACTIVE_TITLE": "Active contacts",
|
||||
"SEARCH_PLACEHOLDER": "Search...",
|
||||
"TITLE": "Əlaqələr",
|
||||
"SEARCH_TITLE": "Əlaqələrdə axtarış",
|
||||
"ACTIVE_TITLE": "Aktiv əlaqələr",
|
||||
"SEARCH_PLACEHOLDER": "Axtarış...",
|
||||
"MESSAGE_BUTTON": "Message",
|
||||
"SEND_MESSAGE": "Send message",
|
||||
"BLOCK_CONTACT": "Block contact",
|
||||
"UNBLOCK_CONTACT": "Unblock contact",
|
||||
"BREADCRUMB": {
|
||||
"CONTACTS": "Contacts"
|
||||
"CONTACTS": "Əlaqələr"
|
||||
},
|
||||
"ACTIONS": {
|
||||
"CONTACT_CREATION": {
|
||||
"ADD_CONTACT": "Add contact",
|
||||
"EXPORT_CONTACT": "Export contacts",
|
||||
"IMPORT_CONTACT": "Import contacts",
|
||||
"SAVE_CONTACT": "Save contact",
|
||||
"ADD_CONTACT": "Əlaqə əlavə et",
|
||||
"EXPORT_CONTACT": "Əlaqələri ixrac et",
|
||||
"IMPORT_CONTACT": "Əlaqələri idxal et",
|
||||
"SAVE_CONTACT": "Əlaqəni yadda saxla",
|
||||
"EMAIL_ADDRESS_DUPLICATE": "This email address is in use for another contact.",
|
||||
"PHONE_NUMBER_DUPLICATE": "This phone number is in use for another contact.",
|
||||
"PHONE_NUMBER_DUPLICATE": "Bu telefon nömrəsi başqa bir əlaqə üçün istifadə olunur.",
|
||||
"SUCCESS_MESSAGE": "Contact saved successfully",
|
||||
"ERROR_MESSAGE": "Unable to save contact. Please try again later."
|
||||
"ERROR_MESSAGE": "Əlaqəni saxlamaq mümkün olmadı. Zəhmət olmasa sonra yenidən cəhd edin."
|
||||
},
|
||||
"BLOCK_SUCCESS_MESSAGE": "This contact is blocked successfully",
|
||||
"BLOCK_ERROR_MESSAGE": "Unable to block contact. Please try again later.",
|
||||
"UNBLOCK_SUCCESS_MESSAGE": "This contact is unblocked successfully",
|
||||
"UNBLOCK_SUCCESS_MESSAGE": "Bu əlaqənin bloku uğurla açıldı",
|
||||
"UNBLOCK_ERROR_MESSAGE": "Unable to unblock contact. Please try again later.",
|
||||
"IMPORT_CONTACT": {
|
||||
"TITLE": "Import contacts",
|
||||
"DESCRIPTION": "Import contacts through a CSV file.",
|
||||
"TITLE": "Əlaqələri idxal et",
|
||||
"DESCRIPTION": "Əlaqələri CSV faylı vasitəsilə idxal edin.",
|
||||
"DOWNLOAD_LABEL": "Download a sample csv.",
|
||||
"LABEL": "CSV File:",
|
||||
"CHOOSE_FILE": "Choose file",
|
||||
"CHANGE": "Change",
|
||||
"CANCEL": "Cancel",
|
||||
"IMPORT": "Import",
|
||||
"SUCCESS_MESSAGE": "You will be notified via email when the import is complete.",
|
||||
"LABEL": "CSV faylı:",
|
||||
"CHOOSE_FILE": "Fayl seçin",
|
||||
"CHANGE": "Dəyiş",
|
||||
"CANCEL": "Ləğv et",
|
||||
"IMPORT": "İdxal et",
|
||||
"SUCCESS_MESSAGE": "İdxal bitdikdə sizə elektron bildiriş göndəriləcək.",
|
||||
"ERROR_MESSAGE": "There was an error, please try again"
|
||||
},
|
||||
"EXPORT_CONTACT": {
|
||||
"TITLE": "Export contacts",
|
||||
"TITLE": "Əlaqələri ixrac et",
|
||||
"DESCRIPTION": "Quickly export a csv file with comprehensive details of your contacts",
|
||||
"CONFIRM": "Export",
|
||||
"SUCCESS_MESSAGE": "Export is in progress. You will be notified on email when the export file is ready to download.",
|
||||
"CONFIRM": "İxrac et",
|
||||
"SUCCESS_MESSAGE": "İxrac davam edir. Fayl hazır olanda sizə elektron bildiriş göndəriləcək.",
|
||||
"ERROR_MESSAGE": "There was an error, please try again"
|
||||
},
|
||||
"SORT_BY": {
|
||||
"LABEL": "Sort by",
|
||||
"LABEL": "Sırala",
|
||||
"OPTIONS": {
|
||||
"NAME": "Name",
|
||||
"EMAIL": "Email",
|
||||
"PHONE_NUMBER": "Phone number",
|
||||
"COMPANY": "Company",
|
||||
"COUNTRY": "Country",
|
||||
"CITY": "City",
|
||||
"LAST_ACTIVITY": "Last activity",
|
||||
"NAME": "Ad",
|
||||
"EMAIL": "Elektron poçt",
|
||||
"PHONE_NUMBER": "Telefon nömrəsi",
|
||||
"COMPANY": "Şirkət",
|
||||
"COUNTRY": "Ölkə",
|
||||
"CITY": "Şəhər",
|
||||
"LAST_ACTIVITY": "Son fəaliyyət",
|
||||
"CREATED_AT": "Created at"
|
||||
}
|
||||
},
|
||||
"ORDER": {
|
||||
"LABEL": "Ordering",
|
||||
"OPTIONS": {
|
||||
"ASCENDING": "Ascending",
|
||||
"ASCENDING": "Artan sıra ilə",
|
||||
"DESCENDING": "Descending"
|
||||
}
|
||||
},
|
||||
@@ -358,17 +358,17 @@
|
||||
"CREATE_SEGMENT": {
|
||||
"TITLE": "Do you want to save this filter?",
|
||||
"CONFIRM": "Save filter",
|
||||
"LABEL": "Name",
|
||||
"LABEL": "Ad",
|
||||
"PLACEHOLDER": "Enter the name of the filter",
|
||||
"ERROR": "Enter a valid name",
|
||||
"ERROR": "Etibarlı ad daxil edin",
|
||||
"SUCCESS_MESSAGE": "Filter saved successfully",
|
||||
"ERROR_MESSAGE": "Unable to save filter. Please try again later."
|
||||
},
|
||||
"DELETE_SEGMENT": {
|
||||
"TITLE": "Confirm Deletion",
|
||||
"DESCRIPTION": "Are you sure you want to delete this filter?",
|
||||
"DESCRIPTION": "Bu filtrin silinməsini təsdiqləyirsiniz?",
|
||||
"CONFIRM": "Yes, Delete",
|
||||
"CANCEL": "No, Cancel",
|
||||
"CANCEL": "Xeyr, Ləğv et",
|
||||
"SUCCESS_MESSAGE": "Filter deleted successfully",
|
||||
"ERROR_MESSAGE": "Unable to delete filter. Please try again later."
|
||||
}
|
||||
@@ -379,18 +379,18 @@
|
||||
"SHOWING": "Showing {startItem} - {endItem} of {totalItems} contacts"
|
||||
},
|
||||
"FILTER": {
|
||||
"NAME": "Name",
|
||||
"EMAIL": "Email",
|
||||
"PHONE_NUMBER": "Phone number",
|
||||
"NAME": "Ad",
|
||||
"EMAIL": "Elektron poçt",
|
||||
"PHONE_NUMBER": "Telefon nömrəsi",
|
||||
"IDENTIFIER": "Identifier",
|
||||
"COUNTRY": "Country",
|
||||
"CITY": "City",
|
||||
"COUNTRY": "Ölkə",
|
||||
"CITY": "Şəhər",
|
||||
"CREATED_AT": "Created at",
|
||||
"LAST_ACTIVITY": "Last activity",
|
||||
"LAST_ACTIVITY": "Son fəaliyyət",
|
||||
"REFERER_LINK": "Referer link",
|
||||
"BLOCKED": "Blocked",
|
||||
"BLOCKED_TRUE": "True",
|
||||
"BLOCKED_FALSE": "False",
|
||||
"BLOCKED_TRUE": "Doğru",
|
||||
"BLOCKED_FALSE": "Yanlış",
|
||||
"BUTTONS": {
|
||||
"CLEAR_FILTERS": "Clear filters",
|
||||
"UPDATE_SEGMENT": "Update segment",
|
||||
@@ -409,7 +409,7 @@
|
||||
}
|
||||
},
|
||||
"CARD": {
|
||||
"OF": "of",
|
||||
"OF": "də",
|
||||
"VIEW_DETAILS": "View details",
|
||||
"EDIT_DETAILS_FORM": {
|
||||
"TITLE": "Edit contact details",
|
||||
@@ -418,27 +418,27 @@
|
||||
"PLACEHOLDER": "Enter the first name"
|
||||
},
|
||||
"LAST_NAME": {
|
||||
"PLACEHOLDER": "Enter the last name"
|
||||
"PLACEHOLDER": "Soyadı daxil edin"
|
||||
},
|
||||
"EMAIL_ADDRESS": {
|
||||
"PLACEHOLDER": "Enter the email address",
|
||||
"DUPLICATE": "This email address is in use for another contact."
|
||||
},
|
||||
"PHONE_NUMBER": {
|
||||
"PLACEHOLDER": "Enter the phone number",
|
||||
"DUPLICATE": "This phone number is in use for another contact."
|
||||
"PLACEHOLDER": "Telefon nömrəsini daxil edin",
|
||||
"DUPLICATE": "Bu telefon nömrəsi başqa bir əlaqə üçün istifadə olunur."
|
||||
},
|
||||
"CITY": {
|
||||
"PLACEHOLDER": "Enter the city name"
|
||||
"PLACEHOLDER": "Şəhər adını daxil edin"
|
||||
},
|
||||
"COUNTRY": {
|
||||
"PLACEHOLDER": "Select country"
|
||||
"PLACEHOLDER": "Ölkəni seçin"
|
||||
},
|
||||
"BIO": {
|
||||
"PLACEHOLDER": "Enter the bio"
|
||||
},
|
||||
"COMPANY_NAME": {
|
||||
"PLACEHOLDER": "Enter the company name"
|
||||
"PLACEHOLDER": "Şirkət adını daxil edin"
|
||||
}
|
||||
},
|
||||
"UPDATE_BUTTON": "Update contact",
|
||||
@@ -477,8 +477,8 @@
|
||||
}
|
||||
},
|
||||
"DETAILS": {
|
||||
"CREATED_AT": "Created {date}",
|
||||
"LAST_ACTIVITY": "Last active {date}",
|
||||
"CREATED_AT": "Yaradılıb {date}",
|
||||
"LAST_ACTIVITY": "Son fəaliyyət {date}",
|
||||
"DELETE_CONTACT_DESCRIPTION": "Permanently delete this contact. This action is irreversible",
|
||||
"DELETE_CONTACT": "Delete contact",
|
||||
"DELETE_DIALOG": {
|
||||
@@ -487,7 +487,7 @@
|
||||
"CONFIRM": "Yes, Delete",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Contact deleted successfully",
|
||||
"ERROR_MESSAGE": "Could not delete contact. Please try again later."
|
||||
"ERROR_MESSAGE": "Əlaqəni silmək mümkün olmadı. Zəhmət olmasa sonra yenidən cəhd edin."
|
||||
}
|
||||
},
|
||||
"AVATAR": {
|
||||
@@ -496,86 +496,86 @@
|
||||
"SUCCESS_MESSAGE": "Avatar uploaded successfully"
|
||||
},
|
||||
"DELETE": {
|
||||
"SUCCESS_MESSAGE": "Avatar deleted successfully",
|
||||
"SUCCESS_MESSAGE": "Avatar uğurla silindi",
|
||||
"ERROR_MESSAGE": "Could not delete avatar. Please try again later."
|
||||
}
|
||||
}
|
||||
},
|
||||
"SIDEBAR": {
|
||||
"TABS": {
|
||||
"ATTRIBUTES": "Attributes",
|
||||
"HISTORY": "History",
|
||||
"NOTES": "Notes",
|
||||
"ATTRIBUTES": "Xüsusiyyətlər",
|
||||
"HISTORY": "Tarix",
|
||||
"NOTES": "Qeydlər",
|
||||
"MERGE": "Merge"
|
||||
},
|
||||
"HISTORY": {
|
||||
"EMPTY_STATE": "There are no previous conversations associated to this contact"
|
||||
"EMPTY_STATE": "Bu əlaqə ilə bağlı əvvəlki söhbətlər yoxdur"
|
||||
},
|
||||
"ATTRIBUTES": {
|
||||
"SEARCH_PLACEHOLDER": "Search for attributes",
|
||||
"UNUSED_ATTRIBUTES": "{count} Used attribute | {count} Unused attributes",
|
||||
"EMPTY_STATE": "There are no contact custom attributes available in this account. You can create a custom attribute in settings.",
|
||||
"YES": "Yes",
|
||||
"NO": "No",
|
||||
"YES": "Bəli",
|
||||
"NO": "Xeyr",
|
||||
"TRIGGER": {
|
||||
"SELECT": "Select value",
|
||||
"INPUT": "Enter value"
|
||||
"SELECT": "Dəyəri seçin",
|
||||
"INPUT": "Dəyər daxil edin"
|
||||
},
|
||||
"VALIDATIONS": {
|
||||
"INVALID_NUMBER": "Invalid number",
|
||||
"REQUIRED": "Valid value is required",
|
||||
"INVALID_INPUT": "Invalid input",
|
||||
"INVALID_URL": "Invalid URL",
|
||||
"INVALID_DATE": "Invalid date"
|
||||
"INVALID_NUMBER": "Yanlış nömrə",
|
||||
"REQUIRED": "Düzgün dəyər tələb olunur",
|
||||
"INVALID_INPUT": "Yanlış giriş",
|
||||
"INVALID_URL": "Yanlış URL",
|
||||
"INVALID_DATE": "Yanlış tarix"
|
||||
},
|
||||
"NO_ATTRIBUTES": "No attributes found",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Attribute updated successfully",
|
||||
"DELETE_SUCCESS_MESSAGE": "Attribute deleted successfully",
|
||||
"UPDATE_ERROR": "Unable to update attribute. Please try again later",
|
||||
"DELETE_ERROR": "Unable to delete attribute. Please try again later"
|
||||
"DELETE_ERROR": "Xüsusiyyəti silmək mümkün olmadı. Zəhmət olmasa, bir az sonra yenidən cəhd edin"
|
||||
}
|
||||
},
|
||||
"MERGE": {
|
||||
"TITLE": "Merge contact",
|
||||
"DESCRIPTION": "Combine two profiles into one, including all attributes and conversations. In case of conflict, the primary contact’s attributes will take precedence.",
|
||||
"PRIMARY": "Primary contact",
|
||||
"DESCRIPTION": "İki profili bütün atributlar və söhbətlər daxil olmaqla birləşdirin. Ziddiyyət olduqda, əsas əlaqənin atributları üstünlük təşkil edəcək.",
|
||||
"PRIMARY": "Əsas əlaqə",
|
||||
"PRIMARY_HELP_LABEL": "To be saved",
|
||||
"PRIMARY_REQUIRED_ERROR": "Please select a contact to merge with before proceeding",
|
||||
"PRIMARY_REQUIRED_ERROR": "Davam etməzdən əvvəl birləşdirmək üçün əlaqə seçin",
|
||||
"PARENT": "To be merged",
|
||||
"PARENT_HELP_LABEL": "To be deleted",
|
||||
"EMPTY_STATE": "No contacts found",
|
||||
"PLACEHOLDER": "Search for primary contact",
|
||||
"SEARCH_PLACEHOLDER": "Search for a contact",
|
||||
"PLACEHOLDER": "Əsas əlaqəni axtar",
|
||||
"SEARCH_PLACEHOLDER": "Əlaqə axtar",
|
||||
"SEARCH_ERROR_MESSAGE": "Could not search for contacts. Please try again later.",
|
||||
"SUCCESS_MESSAGE": "Contact merged successfully",
|
||||
"ERROR_MESSAGE": "Could not merge contacts, try again!",
|
||||
"ERROR_MESSAGE": "Əlaqələri birləşdirmək mümkün olmadı, yenidən cəhd edin!",
|
||||
"IS_SEARCHING": "Searching...",
|
||||
"BUTTONS": {
|
||||
"CANCEL": "Cancel",
|
||||
"CANCEL": "Ləğv et",
|
||||
"CONFIRM": "Merge contact"
|
||||
}
|
||||
},
|
||||
"NOTES": {
|
||||
"PLACEHOLDER": "Add a note",
|
||||
"WROTE": "wrote",
|
||||
"YOU": "You",
|
||||
"PLACEHOLDER": "Qeyd əlavə et",
|
||||
"WROTE": "yazdı",
|
||||
"YOU": "Siz",
|
||||
"SAVE": "Save note",
|
||||
"ADD_NOTE": "Add contact note",
|
||||
"EXPAND": "Expand",
|
||||
"EXPAND": "Genişləndir",
|
||||
"COLLAPSE": "Collapse",
|
||||
"NO_NOTES": "No notes, you can add notes from the contact details page.",
|
||||
"EMPTY_STATE": "There are no notes associated to this contact. You can add a note by typing in the box above.",
|
||||
"CONVERSATION_EMPTY_STATE": "There are no notes yet. Use the Add note button to create one."
|
||||
"CONVERSATION_EMPTY_STATE": "Hələ qeydlər yoxdur. Yeni qeyd yaratmaq üçün Qeyd əlavə et düyməsini istifadə edin."
|
||||
}
|
||||
},
|
||||
"EMPTY_STATE": {
|
||||
"TITLE": "No contacts found in this account",
|
||||
"SUBTITLE": "Start adding new contacts by clicking on the button below",
|
||||
"BUTTON_LABEL": "Add contact",
|
||||
"BUTTON_LABEL": "Əlaqə əlavə et",
|
||||
"SEARCH_EMPTY_STATE_TITLE": "No contacts matches your search 🔍",
|
||||
"LIST_EMPTY_STATE_TITLE": "No contacts available in this view 📋",
|
||||
"ACTIVE_EMPTY_STATE_TITLE": "No contacts are active at the moment 🌙"
|
||||
"LIST_EMPTY_STATE_TITLE": "Bu baxışda əlaqə mövcud deyil 📋",
|
||||
"ACTIVE_EMPTY_STATE_TITLE": "Hazırda aktiv əlaqə yoxdur 🌙"
|
||||
},
|
||||
"LOAD_MORE": "Load more"
|
||||
},
|
||||
@@ -585,12 +585,12 @@
|
||||
"ASSIGN_LABELS_FAILED": "Failed to assign labels",
|
||||
"DESCRIPTION": "Select the labels you want to add to the selected contacts.",
|
||||
"NO_LABELS_FOUND": "No labels available yet.",
|
||||
"SELECTED_COUNT": "{count} selected",
|
||||
"SELECTED_COUNT": "{count} seçildi",
|
||||
"CLEAR_SELECTION": "Clear selection",
|
||||
"SELECT_ALL": "Select all ({count})",
|
||||
"DELETE_CONTACTS": "Delete",
|
||||
"DELETE_SUCCESS": "Contacts deleted successfully.",
|
||||
"DELETE_FAILED": "Failed to delete contacts.",
|
||||
"DELETE_FAILED": "Əlaqələri silmək mümkün olmadı.",
|
||||
"DELETE_DIALOG": {
|
||||
"TITLE": "Delete selected contacts",
|
||||
"SINGULAR_TITLE": "Delete selected contact",
|
||||
@@ -605,27 +605,27 @@
|
||||
"ERROR_MESSAGE": "We couldn’t complete the search. Please try again."
|
||||
},
|
||||
"FORM": {
|
||||
"GO_TO_CONVERSATION": "View",
|
||||
"GO_TO_CONVERSATION": "Bax",
|
||||
"SUCCESS_MESSAGE": "The message was sent successfully!",
|
||||
"ERROR_MESSAGE": "An error occurred while creating the conversation. Please try again later.",
|
||||
"NO_INBOX_ALERT": "There are no available inboxes to start a conversation with this contact.",
|
||||
"CONTACT_SELECTOR": {
|
||||
"LABEL": "To:",
|
||||
"LABEL": "Kimə:",
|
||||
"TAG_INPUT_PLACEHOLDER": "Enter at least 2 characters to search by name, email, or phone number",
|
||||
"CONTACT_CREATING": "Creating contact..."
|
||||
"CONTACT_CREATING": "Əlaqə yaradılır..."
|
||||
},
|
||||
"INBOX_SELECTOR": {
|
||||
"LABEL": "Via:",
|
||||
"BUTTON": "Show inboxes"
|
||||
"BUTTON": "Qutuları göstər"
|
||||
},
|
||||
"EMAIL_OPTIONS": {
|
||||
"SUBJECT_LABEL": "Subject :",
|
||||
"SUBJECT_LABEL": "Mövzu :",
|
||||
"SUBJECT_PLACEHOLDER": "Enter your email subject here",
|
||||
"CC_LABEL": "Cc:",
|
||||
"CC_PLACEHOLDER": "Enter at least 2 characters to search by email",
|
||||
"BCC_LABEL": "Bcc:",
|
||||
"BCC_PLACEHOLDER": "Enter at least 2 characters to search by email",
|
||||
"BCC_BUTTON": "Bcc"
|
||||
"BCC_BUTTON": "Gizli nüsxə"
|
||||
},
|
||||
"MESSAGE_EDITOR": {
|
||||
"PLACEHOLDER": "Write your message here..."
|
||||
@@ -651,8 +651,8 @@
|
||||
}
|
||||
},
|
||||
"ACTION_BUTTONS": {
|
||||
"DISCARD": "Discard",
|
||||
"SEND": "Send ({keyCode})"
|
||||
"DISCARD": "İmtina et",
|
||||
"SEND": "Göndər ({keyCode})"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,452 +1,452 @@
|
||||
{
|
||||
"CONVERSATION": {
|
||||
"SELECT_A_CONVERSATION": "Please select a conversation from left pane",
|
||||
"CSAT_REPLY_MESSAGE": "Please rate the conversation",
|
||||
"404": "Sorry, we cannot find the conversation. Please try again",
|
||||
"SWITCH_VIEW_LAYOUT": "Switch the layout",
|
||||
"DASHBOARD_APP_TAB_MESSAGES": "Messages",
|
||||
"UNVERIFIED_SESSION": "The identity of this user is not verified",
|
||||
"NO_MESSAGE_1": "Uh oh! Looks like there are no messages from customers in your inbox.",
|
||||
"NO_MESSAGE_2": " to send a message to your page!",
|
||||
"NO_INBOX_1": "Hola! Looks like you haven't added any inboxes yet.",
|
||||
"NO_INBOX_2": " to get started",
|
||||
"NO_INBOX_AGENT": "Uh Oh! Looks like you are not part of any inbox. Please contact your administrator",
|
||||
"SEARCH_MESSAGES": "Search for messages in conversations",
|
||||
"VIEW_ORIGINAL": "View original",
|
||||
"VIEW_TRANSLATED": "View translated",
|
||||
"SELECT_A_CONVERSATION": "Zəhmət olmasa, soldakı paneldən bir söhbət seçin",
|
||||
"CSAT_REPLY_MESSAGE": "Zəhmət olmasa söhbəti qiymətləndirin",
|
||||
"404": "Bağışlayın, söhbəti tapa bilmirik. Zəhmət olmasa, yenidən cəhd edin",
|
||||
"SWITCH_VIEW_LAYOUT": "Düzəni dəyişdirin",
|
||||
"DASHBOARD_APP_TAB_MESSAGES": "Mesajlar",
|
||||
"UNVERIFIED_SESSION": "Bu istifadəçinin şəxsiyyəti təsdiqlənməyib",
|
||||
"NO_MESSAGE_1": "Uh oh! Görünür qutunuzda müştərilərdən mesaj yoxdur.",
|
||||
"NO_MESSAGE_2": " səhifənizə mesaj göndərmək üçün!",
|
||||
"NO_INBOX_1": "Hola! Görünür hələ heç bir poçt qutusu əlavə etməmisiniz.",
|
||||
"NO_INBOX_2": " başlamaq üçün",
|
||||
"NO_INBOX_AGENT": "Uh Oh! Görünür heç bir poçt qutusunun üzvü deyilsiniz. Zəhmət olmasa administratorunuzla əlaqə saxlayın",
|
||||
"SEARCH_MESSAGES": "Söhbətlərdə mesajları axtarın",
|
||||
"VIEW_ORIGINAL": "Orijinalı göstər",
|
||||
"VIEW_TRANSLATED": "Tərcüməni göstər",
|
||||
"EMPTY_STATE": {
|
||||
"CMD_BAR": "to open command menu",
|
||||
"KEYBOARD_SHORTCUTS": "to view keyboard shortcuts"
|
||||
"CMD_BAR": "əmr menyusunu açmaq üçün",
|
||||
"KEYBOARD_SHORTCUTS": "klaviatura qısa yollarını görmək üçün"
|
||||
},
|
||||
"SEARCH": {
|
||||
"TITLE": "Search messages",
|
||||
"RESULT_TITLE": "Search Results",
|
||||
"LOADING_MESSAGE": "Crunching data...",
|
||||
"PLACEHOLDER": "Type any text to search messages",
|
||||
"NO_MATCHING_RESULTS": "No results found."
|
||||
"TITLE": "Mesajları axtar",
|
||||
"RESULT_TITLE": "Axtarış Nəticələri",
|
||||
"LOADING_MESSAGE": "Məlumatlar işlənir...",
|
||||
"PLACEHOLDER": "Mesajları axtarmaq üçün istənilən mətni yazın",
|
||||
"NO_MATCHING_RESULTS": "Nəticə tapılmadı."
|
||||
},
|
||||
"UNREAD_MESSAGES": "Unread Messages",
|
||||
"UNREAD_MESSAGE": "Unread Message",
|
||||
"CLICK_HERE": "Click here",
|
||||
"LOADING_INBOXES": "Loading inboxes",
|
||||
"LOADING_CONVERSATIONS": "Loading Conversations",
|
||||
"CANNOT_REPLY": "You cannot reply due to",
|
||||
"24_HOURS_WINDOW": "24 hour message window restriction",
|
||||
"48_HOURS_WINDOW": "48 hour message window restriction",
|
||||
"API_HOURS_WINDOW": "You can only reply to this conversation within {hours} hours",
|
||||
"NOT_ASSIGNED_TO_YOU": "This conversation is not assigned to you. Would you like to assign this conversation to yourself?",
|
||||
"ASSIGN_TO_ME": "Assign to me",
|
||||
"BOT_HANDOFF_MESSAGE": "You are responding to a conversation which is currently handled by an assistant or a bot.",
|
||||
"BOT_HANDOFF_ACTION": "Mark open and assign to you",
|
||||
"BOT_HANDOFF_REOPEN_ACTION": "Mark conversation open",
|
||||
"BOT_HANDOFF_SUCCESS": "Conversation has been handed over to you",
|
||||
"BOT_HANDOFF_ERROR": "Failed to take over the conversation. Please try again.",
|
||||
"TWILIO_WHATSAPP_CAN_REPLY": "You can only reply to this conversation using a template message due to",
|
||||
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "24 hour message window restriction",
|
||||
"OLD_INSTAGRAM_INBOX_REPLY_BANNER": "This Instagram account was migrated to the new Instagram channel inbox. All new messages will show up there. You won’t be able to send messages from this conversation anymore.",
|
||||
"REPLYING_TO": "You are replying to:",
|
||||
"REMOVE_SELECTION": "Remove Selection",
|
||||
"DOWNLOAD": "Download",
|
||||
"UNKNOWN_FILE_TYPE": "Unknown File",
|
||||
"SAVE_CONTACT": "Save Contact",
|
||||
"NO_CONTENT": "No content to display",
|
||||
"UNREAD_MESSAGES": "Oxunmamış Mesajlar",
|
||||
"UNREAD_MESSAGE": "Oxunmamış Mesaj",
|
||||
"CLICK_HERE": "Buraya klik edin",
|
||||
"LOADING_INBOXES": "Qutular yüklənir",
|
||||
"LOADING_CONVERSATIONS": "Söhbətlər yüklənir",
|
||||
"CANNOT_REPLY": "Cavab verə bilməzsiniz, çünki",
|
||||
"24_HOURS_WINDOW": "24 saatlıq mesaj pəncərəsi məhdudiyyəti",
|
||||
"48_HOURS_WINDOW": "48 saatlıq mesaj pəncərəsi məhdudiyyəti",
|
||||
"API_HOURS_WINDOW": "Bu söhbətə yalnız {hours} saat ərzində cavab verə bilərsiniz",
|
||||
"NOT_ASSIGNED_TO_YOU": "Bu söhbət sizə təyin edilməyib. Bu söhbəti özünüzə təyin etmək istərdiniz?",
|
||||
"ASSIGN_TO_ME": "Mənə təyin et",
|
||||
"BOT_HANDOFF_MESSAGE": "Hazırda köməkçi və ya bot tərəfindən idarə olunan söhbətə cavab verirsiniz.",
|
||||
"BOT_HANDOFF_ACTION": "Açıq kimi işarələ və özünüzə təyin et",
|
||||
"BOT_HANDOFF_REOPEN_ACTION": "Söhbəti açıq kimi işarələyin",
|
||||
"BOT_HANDOFF_SUCCESS": "Söhbət sizə təhvil verildi",
|
||||
"BOT_HANDOFF_ERROR": "Söhbəti ələ keçirmək alınmadı. Zəhmət olmasa, yenidən cəhd edin.",
|
||||
"TWILIO_WHATSAPP_CAN_REPLY": "Bu söhbətə yalnız şablon mesajı ilə cavab verə bilərsiniz, çünki",
|
||||
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "24 saatlıq mesaj pəncərəsi məhdudiyyəti",
|
||||
"OLD_INSTAGRAM_INBOX_REPLY_BANNER": "Bu Instagram hesabı yeni Instagram kanalının daxil olan qutusuna köçürülüb. Bütün yeni mesajlar orada görünəcək. Bu söhbətdən artıq mesaj göndərə bilməyəcəksiniz.",
|
||||
"REPLYING_TO": "Siz cavab verirsiniz:",
|
||||
"REMOVE_SELECTION": "Seçimi sil",
|
||||
"DOWNLOAD": "Yüklə",
|
||||
"UNKNOWN_FILE_TYPE": "Naməlum fayl",
|
||||
"SAVE_CONTACT": "Əlaqəni yadda saxla",
|
||||
"NO_CONTENT": "Göstəriləcək məzmun yoxdur",
|
||||
"SHARED_ATTACHMENT": {
|
||||
"CONTACT": "{sender} has shared a contact",
|
||||
"LOCATION": "{sender} has shared a location",
|
||||
"FILE": "{sender} has shared a file",
|
||||
"MEETING": "{sender} has started a meeting"
|
||||
"CONTACT": "{sender} bir əlaqə paylaşıb",
|
||||
"LOCATION": "{sender} bir yer paylaşıb",
|
||||
"FILE": "{sender} bir fayl paylaşıb",
|
||||
"MEETING": "{sender} bir görüş başlayıb"
|
||||
},
|
||||
"UPLOADING_ATTACHMENTS": "Uploading attachments...",
|
||||
"REPLIED_TO_STORY": "Replied to your story",
|
||||
"UPLOADING_ATTACHMENTS": "Əlavələr yüklənir...",
|
||||
"REPLIED_TO_STORY": "Hekayənizə cavab verdi",
|
||||
"UNSUPPORTED_MESSAGE": "This message is unsupported. You can view this message on the Facebook / Instagram app.",
|
||||
"UNSUPPORTED_MESSAGE_FACEBOOK": "This message is unsupported. You can view this message on the Facebook Messenger app.",
|
||||
"UNSUPPORTED_MESSAGE_INSTAGRAM": "This message is unsupported. You can view this message on the Instagram app.",
|
||||
"UNSUPPORTED_MESSAGE_TIKTOK": "This message is unsupported. You can view this message on the TikTok app.",
|
||||
"SUCCESS_DELETE_MESSAGE": "Message deleted successfully",
|
||||
"FAIL_DELETE_MESSSAGE": "Couldn't delete message! Try again",
|
||||
"NO_RESPONSE": "No response",
|
||||
"RESPONSE": "Response",
|
||||
"RATING_TITLE": "Rating",
|
||||
"FEEDBACK_TITLE": "Feedback",
|
||||
"REPLY_MESSAGE_NOT_FOUND": "Message not available",
|
||||
"UNSUPPORTED_MESSAGE_FACEBOOK": "Bu mesaj dəstəklənmir. Bu mesajı Facebook Messenger tətbiqində görə bilərsiniz.",
|
||||
"UNSUPPORTED_MESSAGE_INSTAGRAM": "Bu mesaj dəstəklənmir. Bu mesajı Instagram tətbiqində görə bilərsiniz.",
|
||||
"UNSUPPORTED_MESSAGE_TIKTOK": "Bu mesaj dəstəklənmir. Bu mesajı TikTok tətbiqində görə bilərsiniz.",
|
||||
"SUCCESS_DELETE_MESSAGE": "Mesaj uğurla silindi",
|
||||
"FAIL_DELETE_MESSSAGE": "Mesajı silmək mümkün olmadı! Yenidən cəhd edin",
|
||||
"NO_RESPONSE": "Cavab yoxdur",
|
||||
"RESPONSE": "Cavab",
|
||||
"RATING_TITLE": "Qiymətləndirmə",
|
||||
"FEEDBACK_TITLE": "Rəy",
|
||||
"REPLY_MESSAGE_NOT_FOUND": "Mesaj mövcud deyil",
|
||||
"CARD": {
|
||||
"SHOW_LABELS": "Show labels",
|
||||
"HIDE_LABELS": "Hide labels"
|
||||
"SHOW_LABELS": "Etiketləri göstər",
|
||||
"HIDE_LABELS": "Etiketləri gizlədin"
|
||||
},
|
||||
"VOICE_CALL": {
|
||||
"INCOMING_CALL": "Incoming call",
|
||||
"OUTGOING_CALL": "Outgoing call",
|
||||
"CALL_IN_PROGRESS": "Call in progress",
|
||||
"NO_ANSWER": "No answer",
|
||||
"MISSED_CALL": "Missed call",
|
||||
"CALL_ENDED": "Call ended",
|
||||
"NOT_ANSWERED_YET": "Not answered yet",
|
||||
"THEY_ANSWERED": "They answered",
|
||||
"YOU_ANSWERED": "You answered"
|
||||
"INCOMING_CALL": "Gələn zəng",
|
||||
"OUTGOING_CALL": "Gedən zəng",
|
||||
"CALL_IN_PROGRESS": "Zəng davam edir",
|
||||
"NO_ANSWER": "Cavab yoxdur",
|
||||
"MISSED_CALL": "Qeyri-işlək zəng",
|
||||
"CALL_ENDED": "Zəng bitdi",
|
||||
"NOT_ANSWERED_YET": "Hələ cavab verilməyib",
|
||||
"THEY_ANSWERED": "Onlar cavab verdi",
|
||||
"YOU_ANSWERED": "Siz cavab verdiniz"
|
||||
},
|
||||
"HEADER": {
|
||||
"RESOLVE_ACTION": "Resolve",
|
||||
"REOPEN_ACTION": "Reopen",
|
||||
"OPEN_ACTION": "Open",
|
||||
"MORE_ACTIONS": "More actions",
|
||||
"OPEN": "More",
|
||||
"CLOSE": "Close",
|
||||
"DETAILS": "details",
|
||||
"SNOOZED_UNTIL": "Snoozed until",
|
||||
"SNOOZED_UNTIL_TOMORROW": "Snoozed until tomorrow",
|
||||
"SNOOZED_UNTIL_NEXT_WEEK": "Snoozed until next week",
|
||||
"SNOOZED_UNTIL_NEXT_REPLY": "Snoozed until next reply",
|
||||
"RESOLVE_ACTION": "Həll et",
|
||||
"REOPEN_ACTION": "Yenidən aç",
|
||||
"OPEN_ACTION": "Aç",
|
||||
"MORE_ACTIONS": "Daha çox əməliyyat",
|
||||
"OPEN": "Daha çox",
|
||||
"CLOSE": "Bağla",
|
||||
"DETAILS": "təfərrüatlar",
|
||||
"SNOOZED_UNTIL": "Gecikdirilib",
|
||||
"SNOOZED_UNTIL_TOMORROW": "Sabaha qədər təxirə salındı",
|
||||
"SNOOZED_UNTIL_NEXT_WEEK": "Gələn həftəyə qədər təxirə salındı",
|
||||
"SNOOZED_UNTIL_NEXT_REPLY": "Növbəti cavaba qədər təxirə salındı",
|
||||
"SLA_STATUS": {
|
||||
"FRT": "FRT {status}",
|
||||
"NRT": "NRT {status}",
|
||||
"RT": "RT {status}",
|
||||
"MISSED": "missed",
|
||||
"DUE": "due"
|
||||
"MISSED": "qaçırıldı",
|
||||
"DUE": "vaxtı çatıb"
|
||||
}
|
||||
},
|
||||
"RESOLVE_DROPDOWN": {
|
||||
"MARK_PENDING": "Mark as pending",
|
||||
"SNOOZE_UNTIL": "Snooze",
|
||||
"MARK_PENDING": "Gözləmədə kimi işarələyin",
|
||||
"SNOOZE_UNTIL": "Gecikdir",
|
||||
"SNOOZE": {
|
||||
"TITLE": "Snooze until",
|
||||
"NEXT_REPLY": "Next reply",
|
||||
"TOMORROW": "Tomorrow",
|
||||
"NEXT_WEEK": "Next week"
|
||||
"TITLE": "Gecikdirin, qədər",
|
||||
"NEXT_REPLY": "Növbəti cavab",
|
||||
"TOMORROW": "Sabah",
|
||||
"NEXT_WEEK": "Gələn həftə"
|
||||
}
|
||||
},
|
||||
"MENTION": {
|
||||
"AGENTS": "Agents",
|
||||
"TEAMS": "Teams"
|
||||
"AGENTS": "Agentlər",
|
||||
"TEAMS": "Komandalar"
|
||||
},
|
||||
"CUSTOM_SNOOZE": {
|
||||
"TITLE": "Snooze until",
|
||||
"APPLY": "Snooze",
|
||||
"CANCEL": "Cancel"
|
||||
"TITLE": "Gecikdirmə müddəti",
|
||||
"APPLY": "Gecikdir",
|
||||
"CANCEL": "Ləğv et"
|
||||
},
|
||||
"PRIORITY": {
|
||||
"TITLE": "Priority",
|
||||
"TITLE": "Prioritet",
|
||||
"OPTIONS": {
|
||||
"NONE": "None",
|
||||
"URGENT": "Urgent",
|
||||
"HIGH": "High",
|
||||
"MEDIUM": "Medium",
|
||||
"LOW": "Low"
|
||||
"NONE": "Heç biri",
|
||||
"URGENT": "Təcili",
|
||||
"HIGH": "Yüksək",
|
||||
"MEDIUM": "Orta",
|
||||
"LOW": "Aşağı"
|
||||
},
|
||||
"CHANGE_PRIORITY": {
|
||||
"SELECT_PLACEHOLDER": "None",
|
||||
"INPUT_PLACEHOLDER": "Select priority",
|
||||
"NO_RESULTS": "No results found",
|
||||
"SUCCESSFUL": "Changed priority of conversation id {conversationId} to {priority}",
|
||||
"FAILED": "Couldn't change priority. Please try again."
|
||||
"SELECT_PLACEHOLDER": "Heç biri",
|
||||
"INPUT_PLACEHOLDER": "Prioritet seçin",
|
||||
"NO_RESULTS": "Nəticə tapılmadı",
|
||||
"SUCCESSFUL": "{conversationId} söhbətinin prioriteti {priority} olaraq dəyişdirildi",
|
||||
"FAILED": "Prioritet dəyişdirilə bilmədi. Zəhmət olmasa yenidən cəhd edin."
|
||||
}
|
||||
},
|
||||
"DELETE_CONVERSATION": {
|
||||
"TITLE": "Delete conversation #{conversationId}",
|
||||
"DESCRIPTION": "Are you sure you want to delete this conversation?",
|
||||
"CONFIRM": "Delete"
|
||||
"TITLE": "#{conversationId} nömrəli söhbəti sil",
|
||||
"DESCRIPTION": "Bu söhbəti silmək istədiyinizə əminsiniz?",
|
||||
"CONFIRM": "Sil"
|
||||
},
|
||||
"CARD_CONTEXT_MENU": {
|
||||
"PENDING": "Mark as pending",
|
||||
"RESOLVED": "Mark as resolved",
|
||||
"MARK_AS_UNREAD": "Mark as unread",
|
||||
"MARK_AS_READ": "Mark as read",
|
||||
"REOPEN": "Reopen conversation",
|
||||
"PENDING": "Gözləyən kimi işarələyin",
|
||||
"RESOLVED": "Həll olundu kimi işarələyin",
|
||||
"MARK_AS_UNREAD": "Oxunmamış kimi işarələyin",
|
||||
"MARK_AS_READ": "Oxunmuş kimi işarələ",
|
||||
"REOPEN": "Söhbəti yenidən açın",
|
||||
"SNOOZE": {
|
||||
"TITLE": "Snooze",
|
||||
"NEXT_REPLY": "Until next reply",
|
||||
"TOMORROW": "Until tomorrow",
|
||||
"NEXT_WEEK": "Until next week"
|
||||
"TITLE": "Gecikdir",
|
||||
"NEXT_REPLY": "Növbəti cavaba qədər",
|
||||
"TOMORROW": "Sabaha qədər",
|
||||
"NEXT_WEEK": "Növbəti həftəyə qədər"
|
||||
},
|
||||
"ASSIGN_AGENT": "Assign agent",
|
||||
"ASSIGN_LABEL": "Assign label",
|
||||
"AGENTS_LOADING": "Loading agents...",
|
||||
"ASSIGN_TEAM": "Assign team",
|
||||
"DELETE": "Delete conversation",
|
||||
"OPEN_IN_NEW_TAB": "Open in new tab",
|
||||
"COPY_LINK": "Copy conversation link",
|
||||
"COPY_LINK_SUCCESS": "Conversation link copied to clipboard",
|
||||
"ASSIGN_AGENT": "Agent təyin et",
|
||||
"ASSIGN_LABEL": "Etiket təyin et",
|
||||
"AGENTS_LOADING": "Agentlər yüklənir...",
|
||||
"ASSIGN_TEAM": "Komandaya təyin et",
|
||||
"DELETE": "Söhbəti sil",
|
||||
"OPEN_IN_NEW_TAB": "Yeni nişanda aç",
|
||||
"COPY_LINK": "Söhbət linkini kopyala",
|
||||
"COPY_LINK_SUCCESS": "Söhbət linki panoya kopyalandı",
|
||||
"API": {
|
||||
"AGENT_ASSIGNMENT": {
|
||||
"SUCCESFUL": "Conversation id {conversationId} assigned to \"{agentName}\"",
|
||||
"FAILED": "Couldn't assign agent. Please try again."
|
||||
"SUCCESFUL": "{conversationId} söhbəti \"{agentName}\" agentinə təyin edildi",
|
||||
"FAILED": "Agent təyin etmək mümkün olmadı. Zəhmət olmasa yenidən cəhd edin."
|
||||
},
|
||||
"LABEL_ASSIGNMENT": {
|
||||
"SUCCESFUL": "Assigned label #{labelName} to conversation id {conversationId}",
|
||||
"FAILED": "Couldn't assign label. Please try again."
|
||||
"SUCCESFUL": "{conversationId} söhbətinə #{labelName} etiketi təyin edildi",
|
||||
"FAILED": "Etiket təyin etmək mümkün olmadı. Zəhmət olmasa yenidən cəhd edin."
|
||||
},
|
||||
"LABEL_REMOVAL": {
|
||||
"SUCCESFUL": "Removed label #{labelName} from conversation id {conversationId}",
|
||||
"FAILED": "Couldn't remove label. Please try again."
|
||||
"SUCCESFUL": "{conversationId} nömrəli söhbətdən #{labelName} etiketi silindi",
|
||||
"FAILED": "Etiketi silmək mümkün olmadı. Zəhmət olmasa yenidən cəhd edin."
|
||||
},
|
||||
"TEAM_ASSIGNMENT": {
|
||||
"SUCCESFUL": "Assigned team \"{team}\" to conversation id {conversationId}",
|
||||
"FAILED": "Couldn't assign team. Please try again."
|
||||
"SUCCESFUL": "Söhbət id-si {conversationId} üçün \"{team}\" komandası təyin edildi",
|
||||
"FAILED": "Komanda təyin etmək mümkün olmadı. Zəhmət olmasa yenidən cəhd edin."
|
||||
}
|
||||
}
|
||||
},
|
||||
"FOOTER": {
|
||||
"MESSAGE_SIGN_TOOLTIP": "Message signature",
|
||||
"ENABLE_SIGN_TOOLTIP": "Enable signature",
|
||||
"DISABLE_SIGN_TOOLTIP": "Disable signature",
|
||||
"MSG_INPUT": "Shift + enter for new line. Start with '/' to select a Canned Response.",
|
||||
"PRIVATE_MSG_INPUT": "Shift + enter for new line. This will be visible only to Agents",
|
||||
"MESSAGING_RESTRICTED": "You cannot reply to this conversation",
|
||||
"MESSAGING_RESTRICTED_WHATSAPP": "You can only reply using a template message due to 24-hour message window restriction",
|
||||
"MESSAGING_RESTRICTED_API": "You can only reply using a template message due to message window restriction",
|
||||
"MESSAGE_SIGNATURE_NOT_CONFIGURED": "Message signature is not configured, please configure it in profile settings.",
|
||||
"COPILOT_MSG_INPUT": "Give copilot additional prompts, or ask anything else... Press enter to send follow-up",
|
||||
"CLICK_HERE": "Click here to update",
|
||||
"WHATSAPP_TEMPLATES": "Whatsapp Templates"
|
||||
"MESSAGE_SIGN_TOOLTIP": "Mesaj imzası",
|
||||
"ENABLE_SIGN_TOOLTIP": "İmzaya icazə ver",
|
||||
"DISABLE_SIGN_TOOLTIP": "İmzaya icazə vermə",
|
||||
"MSG_INPUT": "Yeni sətr üçün Shift + enter. Canned Response seçmək üçün '/' ilə başlayın.",
|
||||
"PRIVATE_MSG_INPUT": "Yeni sətr üçün Shift + enter. Bu yalnız Agentlər üçün görünəcək",
|
||||
"MESSAGING_RESTRICTED": "Bu söhbətə cavab verə bilməzsiniz",
|
||||
"MESSAGING_RESTRICTED_WHATSAPP": "24 saatlıq mesaj pəncərəsi məhdudiyyəti səbəbindən yalnız şablon mesajla cavab verə bilərsiniz",
|
||||
"MESSAGING_RESTRICTED_API": "Mesaj pəncərəsi məhdudiyyəti səbəbindən yalnız şablon mesajla cavab verə bilərsiniz",
|
||||
"MESSAGE_SIGNATURE_NOT_CONFIGURED": "Mesaj imzası qurulmayıb, zəhmət olmasa profil parametrlərində qurun.",
|
||||
"COPILOT_MSG_INPUT": "Copilot üçün əlavə göstərişlər verin və ya başqa sual verin... Davam etmək üçün enter düyməsini basın",
|
||||
"CLICK_HERE": "Yeniləmək üçün buraya klikləyin",
|
||||
"WHATSAPP_TEMPLATES": "Whatsapp Şablonları"
|
||||
},
|
||||
"REPLYBOX": {
|
||||
"REPLY": "Reply",
|
||||
"PRIVATE_NOTE": "Private Note",
|
||||
"SEND": "Send",
|
||||
"CREATE": "Add Note",
|
||||
"INSERT_READ_MORE": "Read more",
|
||||
"DISMISS_REPLY": "Dismiss reply",
|
||||
"REPLYING_TO": "Replying to:",
|
||||
"TIP_EMOJI_ICON": "Show emoji selector",
|
||||
"TIP_ATTACH_ICON": "Attach files",
|
||||
"TIP_AUDIORECORDER_ICON": "Record audio",
|
||||
"TIP_AUDIORECORDER_PERMISSION": "Allow access to audio",
|
||||
"TIP_AUDIORECORDER_ERROR": "Could not open the audio",
|
||||
"DRAG_DROP": "Drag and drop here to attach",
|
||||
"START_AUDIO_RECORDING": "Start audio recording",
|
||||
"STOP_AUDIO_RECORDING": "Stop audio recording",
|
||||
"COPILOT_THINKING": "Copilot is thinking",
|
||||
"REPLY": "Cavab ver",
|
||||
"PRIVATE_NOTE": "Şəxsi Qeyd",
|
||||
"SEND": "Göndər",
|
||||
"CREATE": "Qeyd əlavə et",
|
||||
"INSERT_READ_MORE": "Daha çox oxu",
|
||||
"DISMISS_REPLY": "Cavabı ləğv et",
|
||||
"REPLYING_TO": "Cavab verir:",
|
||||
"TIP_EMOJI_ICON": "Emoji seçicisini göstər",
|
||||
"TIP_ATTACH_ICON": "Faylları əlavə et",
|
||||
"TIP_AUDIORECORDER_ICON": "Səs yaz",
|
||||
"TIP_AUDIORECORDER_PERMISSION": "Səsə girişə icazə ver",
|
||||
"TIP_AUDIORECORDER_ERROR": "Səsi açmaq mümkün olmadı",
|
||||
"DRAG_DROP": "Qoşmaq üçün buraya sürükləyin və buraxın",
|
||||
"START_AUDIO_RECORDING": "Səs yazısını başla",
|
||||
"STOP_AUDIO_RECORDING": "Səs yazısını dayandırın",
|
||||
"COPILOT_THINKING": "Copilot düşünür",
|
||||
"EMAIL_HEAD": {
|
||||
"TO": "TO",
|
||||
"ADD_BCC": "Add bcc",
|
||||
"TO": "KİMƏ",
|
||||
"ADD_BCC": "Gizli nüsxə əlavə et",
|
||||
"CC": {
|
||||
"LABEL": "CC",
|
||||
"PLACEHOLDER": "Emails separated by commas",
|
||||
"ERROR": "Please enter valid email addresses"
|
||||
"PLACEHOLDER": "Vergüllə ayrılmış elektron poçtlar",
|
||||
"ERROR": "Zəhmət olmasa düzgün elektron poçt ünvanları daxil edin"
|
||||
},
|
||||
"BCC": {
|
||||
"LABEL": "BCC",
|
||||
"PLACEHOLDER": "Emails separated by commas",
|
||||
"ERROR": "Please enter valid email addresses"
|
||||
"PLACEHOLDER": "Vergüllə ayrılmış e-poçtlar",
|
||||
"ERROR": "Zəhmət olmasa, düzgün e-poçt ünvanları daxil edin"
|
||||
}
|
||||
},
|
||||
"UNDEFINED_VARIABLES": {
|
||||
"TITLE": "Undefined variables",
|
||||
"MESSAGE": "You have {undefinedVariablesCount} undefined variables in your message: {undefinedVariables}. Would you like to send the message anyway?",
|
||||
"TITLE": "Təyin olunmamış dəyişənlər",
|
||||
"MESSAGE": "Mesajınızda {undefinedVariablesCount} təyin olunmamış dəyişən var: {undefinedVariables}. Mesajı yenə də göndərmək istəyirsiniz?",
|
||||
"CONFIRM": {
|
||||
"YES": "Send",
|
||||
"CANCEL": "Cancel"
|
||||
"YES": "Göndər",
|
||||
"CANCEL": "Ləğv et"
|
||||
}
|
||||
},
|
||||
"QUOTED_REPLY": {
|
||||
"ENABLE_TOOLTIP": "Include quoted email thread",
|
||||
"DISABLE_TOOLTIP": "Don't include quoted email thread",
|
||||
"REMOVE_PREVIEW": "Remove quoted email thread",
|
||||
"COLLAPSE": "Collapse preview",
|
||||
"EXPAND": "Expand preview"
|
||||
"ENABLE_TOOLTIP": "Sitat gətirilmiş e-poçt mövzusunu daxil et",
|
||||
"DISABLE_TOOLTIP": "Sitat gətirilmiş e-poçt mövzusunu daxil etmə",
|
||||
"REMOVE_PREVIEW": "Sitat gətirilmiş e-poçt mövzusunu sil",
|
||||
"COLLAPSE": "Önizləməni yığışdır",
|
||||
"EXPAND": "Önizləməni genişləndir"
|
||||
}
|
||||
},
|
||||
"VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team",
|
||||
"CHANGE_STATUS": "Conversation status changed",
|
||||
"CHANGE_STATUS_FAILED": "Conversation status change failed",
|
||||
"CHANGE_AGENT": "Conversation Assignee changed",
|
||||
"CHANGE_AGENT_FAILED": "Assignee change failed",
|
||||
"ASSIGN_LABEL_SUCCESFUL": "Label assigned successfully",
|
||||
"ASSIGN_LABEL_FAILED": "Label assignment failed",
|
||||
"CHANGE_TEAM": "Conversation team changed",
|
||||
"SUCCESS_DELETE_CONVERSATION": "Conversation deleted successfully",
|
||||
"FAIL_DELETE_CONVERSATION": "Couldn't delete conversation! Try again",
|
||||
"FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE} MB attachment limit",
|
||||
"FILE_TYPE_NOT_SUPPORTED": "This {fileName} file type is not supported in this conversation",
|
||||
"MESSAGE_ERROR": "Unable to send this message, please try again later",
|
||||
"SENT_BY": "Sent by:",
|
||||
"VISIBLE_TO_AGENTS": "Şəxsi Qeyd: Yalnız siz və komandanız üçün görünür",
|
||||
"CHANGE_STATUS": "Söhbət statusu dəyişdirildi",
|
||||
"CHANGE_STATUS_FAILED": "Söhbətin statusunu dəyişmək mümkün olmadı",
|
||||
"CHANGE_AGENT": "Söhbətin məsul şəxsi dəyişdirildi",
|
||||
"CHANGE_AGENT_FAILED": "Təyinat dəyişdirilməsi uğursuz oldu",
|
||||
"ASSIGN_LABEL_SUCCESFUL": "Etiket uğurla təyin edildi",
|
||||
"ASSIGN_LABEL_FAILED": "Etiket təyini uğursuz oldu",
|
||||
"CHANGE_TEAM": "Söhbət komandası dəyişdirildi",
|
||||
"SUCCESS_DELETE_CONVERSATION": "Söhbət uğurla silindi",
|
||||
"FAIL_DELETE_CONVERSATION": "Söhbəti silmək mümkün olmadı! Yenidən cəhd edin",
|
||||
"FILE_SIZE_LIMIT": "Fayl {MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE} MB əlavə limitini aşır",
|
||||
"FILE_TYPE_NOT_SUPPORTED": "Bu {fileName} fayl növü bu söhbətdə dəstəklənmir",
|
||||
"MESSAGE_ERROR": "Bu mesajı göndərmək mümkün olmadı, zəhmət olmasa bir az sonra yenidən cəhd edin",
|
||||
"SENT_BY": "Göndərən:",
|
||||
"BOT": "Bot",
|
||||
"NATIVE_APP": "Native app",
|
||||
"NATIVE_APP_ADVISORY": "This message was sent from the native app. Reply from Chatwoot to maintain the message window.",
|
||||
"SEND_FAILED": "Couldn't send message! Try again",
|
||||
"TRY_AGAIN": "retry",
|
||||
"NATIVE_APP": "Yerli tətbiq",
|
||||
"NATIVE_APP_ADVISORY": "Bu mesaj yerli tətbiqdən göndərilib. Mesaj pəncərəsini saxlamaq üçün Chatwoot-dan cavab verin.",
|
||||
"SEND_FAILED": "Mesaj göndərmək mümkün olmadı! Yenidən cəhd edin",
|
||||
"TRY_AGAIN": "yenidən cəhd et",
|
||||
"ASSIGNMENT": {
|
||||
"SELECT_AGENT": "Select Agent",
|
||||
"REMOVE": "Remove",
|
||||
"ASSIGN": "Assign"
|
||||
"SELECT_AGENT": "Agent seçin",
|
||||
"REMOVE": "Sil",
|
||||
"ASSIGN": "Təyin et"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"COPY": "Copy",
|
||||
"REPLY_TO": "Reply to this message",
|
||||
"DELETE": "Delete",
|
||||
"CREATE_A_CANNED_RESPONSE": "Add to canned responses",
|
||||
"TRANSLATE": "Translate",
|
||||
"COPY_PERMALINK": "Copy link to the message",
|
||||
"LINK_COPIED": "Message URL copied to the clipboard",
|
||||
"COPY": "Kopyala",
|
||||
"REPLY_TO": "Bu mesaja cavab ver",
|
||||
"DELETE": "Sil",
|
||||
"CREATE_A_CANNED_RESPONSE": "Hazır cavablara əlavə et",
|
||||
"TRANSLATE": "Tərcümə et",
|
||||
"COPY_PERMALINK": "Mesaja keçid linkini kopyalayın",
|
||||
"LINK_COPIED": "Mesajın URL-i panoya kopyalandı",
|
||||
"DELETE_CONFIRMATION": {
|
||||
"TITLE": "Are you sure you want to delete this message?",
|
||||
"MESSAGE": "You cannot undo this action",
|
||||
"DELETE": "Delete",
|
||||
"CANCEL": "Cancel"
|
||||
"TITLE": "Bu mesajı silmək istədiyinizə əminsiniz?",
|
||||
"MESSAGE": "Bu əməliyyatı geri qaytara bilməzsiniz",
|
||||
"DELETE": "Sil",
|
||||
"CANCEL": "Ləğv et"
|
||||
}
|
||||
},
|
||||
"SIDEBAR": {
|
||||
"CONTACT": "Contact",
|
||||
"CONTACT": "Əlaqə",
|
||||
"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"
|
||||
"INCOMING_CALL": "Gələn zəng",
|
||||
"OUTGOING_CALL": "Gedən zəng",
|
||||
"CALL_IN_PROGRESS": "Zəng davam edir",
|
||||
"NOT_ANSWERED_YET": "Hələ cavab verilməyib",
|
||||
"HANDLED_IN_ANOTHER_TAB": "Başqa sekmədə işlənir",
|
||||
"REJECT_CALL": "İmtina et",
|
||||
"JOIN_CALL": "Zəngə qoşul",
|
||||
"END_CALL": "Zəngi bitir"
|
||||
}
|
||||
},
|
||||
"EMAIL_TRANSCRIPT": {
|
||||
"TITLE": "Send conversation transcript",
|
||||
"DESC": "Send a copy of the conversation transcript to the specified email address",
|
||||
"SUBMIT": "Submit",
|
||||
"CANCEL": "Cancel",
|
||||
"SEND_EMAIL_SUCCESS": "The chat transcript was sent successfully",
|
||||
"SEND_EMAIL_ERROR": "There was an error, please try again",
|
||||
"SEND_EMAIL_PAYMENT_REQUIRED": "Email transcript is not available on your current plan. Please upgrade to use this feature.",
|
||||
"TITLE": "Söhbət transkriptini göndər",
|
||||
"DESC": "Söhbət transkriptinin surətini göstərilən e-poçt ünvanına göndərin",
|
||||
"SUBMIT": "Təsdiqlə",
|
||||
"CANCEL": "Ləğv et",
|
||||
"SEND_EMAIL_SUCCESS": "Söhbət yazısı uğurla göndərildi",
|
||||
"SEND_EMAIL_ERROR": "Xəta baş verdi, zəhmət olmasa yenidən cəhd edin",
|
||||
"SEND_EMAIL_PAYMENT_REQUIRED": "Cari planınızda e-poçt yazışması mövcud deyil. Bu funksiyanı istifadə etmək üçün lütfən planınızı yüksəldin.",
|
||||
"FORM": {
|
||||
"SEND_TO_CONTACT": "Send the transcript to the customer",
|
||||
"SEND_TO_AGENT": "Send the transcript to the assigned agent",
|
||||
"SEND_TO_OTHER_EMAIL_ADDRESS": "Send the transcript to another email address",
|
||||
"SEND_TO_CONTACT": "Yazını müştəriyə göndər",
|
||||
"SEND_TO_AGENT": "Mətni təyin olunmuş agentə göndər",
|
||||
"SEND_TO_OTHER_EMAIL_ADDRESS": "Yazını başqa e-poçt ünvanına göndər",
|
||||
"EMAIL": {
|
||||
"PLACEHOLDER": "Enter an email address",
|
||||
"ERROR": "Please enter a valid email address"
|
||||
"PLACEHOLDER": "E-poçt ünvanı daxil edin",
|
||||
"ERROR": "Zəhmət olmasa, düzgün e-poçt ünvanı daxil edin"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ONBOARDING": {
|
||||
"TITLE": "Hey 👋, Welcome to {installationName}!",
|
||||
"DESCRIPTION": "Thanks for signing up. We want you to get the most out of {installationName}. Here are a few things you can do in {installationName} to make the experience delightful.",
|
||||
"GREETING_MORNING": "👋 Good morning, {name}. Welcome to {installationName}.",
|
||||
"GREETING_AFTERNOON": "👋 Good afternoon, {name}. Welcome to {installationName}.",
|
||||
"GREETING_EVENING": "👋 Good evening, {name}. Welcome to {installationName}.",
|
||||
"READ_LATEST_UPDATES": "Read our latest updates",
|
||||
"TITLE": "Salam 👋, {installationName}-ə xoş gəlmisiniz!",
|
||||
"DESCRIPTION": "Qeydiyyatdan keçdiyiniz üçün təşəkkür edirik. {installationName}-dən maksimum faydalanmağınızı istəyirik. Təcrübəni xoş etmək üçün {installationName}-də edə biləcəyiniz bir neçə şey var.",
|
||||
"GREETING_MORNING": "👋 Sabahınız xeyir, {name}. {installationName}-ə xoş gəlmisiniz.",
|
||||
"GREETING_AFTERNOON": "👋 Günortanız xeyir, {name}. {installationName}-ə xoş gəlmisiniz.",
|
||||
"GREETING_EVENING": "👋 Axşamınız xeyir, {name}. {installationName}-ə xoş gəlmisiniz.",
|
||||
"READ_LATEST_UPDATES": "Ən son yeniliklərimizi oxuyun",
|
||||
"ALL_CONVERSATION": {
|
||||
"TITLE": "All your conversations in one place",
|
||||
"DESCRIPTION": "View all the conversations from your customers in one single dashboard. You can filter the conversations by the incoming channel, label and status.",
|
||||
"NEW_LINK": "Click here to create an inbox"
|
||||
"TITLE": "Bütün söhbətləriniz bir yerdə",
|
||||
"DESCRIPTION": "Müştərilərinizdən olan bütün söhbətləri tək bir paneldə görün. Söhbətləri daxil olan kanal, etiket və vəziyyətə görə süzgəcdən keçirə bilərsiniz.",
|
||||
"NEW_LINK": "Qutunu yaratmaq üçün bura klikləyin"
|
||||
},
|
||||
"TEAM_MEMBERS": {
|
||||
"TITLE": "Invite your team members",
|
||||
"DESCRIPTION": "Since you are getting ready to talk to your customer, bring in your teammates to assist you. You can invite your teammates by adding their email addresses to the agent list.",
|
||||
"NEW_LINK": "Click here to invite a team member"
|
||||
"TITLE": "Komanda üzvlərinizi dəvət edin",
|
||||
"DESCRIPTION": "Müştərinizlə danışmağa hazırlaşdığınız üçün, sizə kömək etmək üçün komanda üzvlərinizi dəvət edin. Komanda üzvlərinizi agent siyahısına onların e-poçt ünvanlarını əlavə etməklə dəvət edə bilərsiniz.",
|
||||
"NEW_LINK": "Komanda üzvünü dəvət etmək üçün buraya klikləyin"
|
||||
},
|
||||
"LABELS": {
|
||||
"TITLE": "Organize conversations with labels",
|
||||
"DESCRIPTION": "Labels provide an easier way to categorize your conversation. Create some labels like #support-enquiry, #billing-question etc., so that you can use them in a conversation later.",
|
||||
"NEW_LINK": "Click here to create tags"
|
||||
"TITLE": "Söhbətləri etiketlərlə təşkil edin",
|
||||
"DESCRIPTION": "Etiketlər söhbətinizi kateqoriyalara ayırmağı asanlaşdırır. Sonra söhbətdə istifadə etmək üçün #support-enquiry, #billing-question və s. kimi bəzi etiketlər yaradın.",
|
||||
"NEW_LINK": "Etiket yaratmaq üçün buraya klikləyin"
|
||||
},
|
||||
"CANNED_RESPONSES": {
|
||||
"TITLE": "Create canned responses",
|
||||
"DESCRIPTION": "Pre-written quick reply templates help you quickly respond to a conversation. Agents can type the '/' character followed by the shortcode to insert a response.",
|
||||
"NEW_LINK": "Click here to create a canned response"
|
||||
"TITLE": "Hazır cavablar yaradın",
|
||||
"DESCRIPTION": "Əvvəlcədən yazılmış sürətli cavab şablonları söhbətə tez cavab verməyinizə kömək edir. Agentlər cavaba daxil etmək üçün '/' simvolunu və sonra qısa kodu yaza bilərlər.",
|
||||
"NEW_LINK": "Sürətli cavab yaratmaq üçün bura klikləyin"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_SIDEBAR": {
|
||||
"ASSIGNEE_LABEL": "Assigned Agent",
|
||||
"SELF_ASSIGN": "Assign to me",
|
||||
"TEAM_LABEL": "Assigned Team",
|
||||
"ASSIGNEE_LABEL": "Təyin olunmuş agent",
|
||||
"SELF_ASSIGN": "Mənə təyin et",
|
||||
"TEAM_LABEL": "Təyin olunmuş komanda",
|
||||
"SELECT": {
|
||||
"PLACEHOLDER": "None"
|
||||
"PLACEHOLDER": "Heç biri"
|
||||
},
|
||||
"ACCORDION": {
|
||||
"CONTACT_DETAILS": "Contact Details",
|
||||
"CONVERSATION_ACTIONS": "Conversation Actions",
|
||||
"CONVERSATION_LABELS": "Conversation Labels",
|
||||
"CONVERSATION_INFO": "Conversation Information",
|
||||
"CONTACT_NOTES": "Contact Notes",
|
||||
"CONTACT_ATTRIBUTES": "Contact Attributes",
|
||||
"PREVIOUS_CONVERSATION": "Previous Conversations",
|
||||
"MACROS": "Macros",
|
||||
"LINEAR_ISSUES": "Linked Linear Issues",
|
||||
"SHOPIFY_ORDERS": "Shopify Orders"
|
||||
"CONTACT_DETAILS": "Əlaqə Məlumatları",
|
||||
"CONVERSATION_ACTIONS": "Söhbət Əməliyyatları",
|
||||
"CONVERSATION_LABELS": "Söhbət Etiketləri",
|
||||
"CONVERSATION_INFO": "Söhbət Məlumatları",
|
||||
"CONTACT_NOTES": "Əlaqə Qeydləri",
|
||||
"CONTACT_ATTRIBUTES": "Əlaqə Xüsusiyyətləri",
|
||||
"PREVIOUS_CONVERSATION": "Əvvəlki Söhbətlər",
|
||||
"MACROS": "Makrolar",
|
||||
"LINEAR_ISSUES": "Əlaqəli Linear məsələlər",
|
||||
"SHOPIFY_ORDERS": "Shopify Sifarişləri"
|
||||
},
|
||||
"SHOPIFY": {
|
||||
"ORDER_ID": "Order #{id}",
|
||||
"ERROR": "Error loading orders",
|
||||
"NO_SHOPIFY_ORDERS": "No orders found",
|
||||
"ORDER_ID": "Sifariş #{id}",
|
||||
"ERROR": "Sifarişlərin yüklənməsində xəta",
|
||||
"NO_SHOPIFY_ORDERS": "Sifariş tapılmadı",
|
||||
"FINANCIAL_STATUS": {
|
||||
"PENDING": "Pending",
|
||||
"AUTHORIZED": "Authorized",
|
||||
"PARTIALLY_PAID": "Partially Paid",
|
||||
"PAID": "Paid",
|
||||
"PARTIALLY_REFUNDED": "Partially Refunded",
|
||||
"REFUNDED": "Refunded",
|
||||
"VOIDED": "Voided"
|
||||
"PENDING": "Gözləmədə",
|
||||
"AUTHORIZED": "Təsdiqlənmiş",
|
||||
"PARTIALLY_PAID": "Qismən ödənilmiş",
|
||||
"PAID": "Ödənilib",
|
||||
"PARTIALLY_REFUNDED": "Qismən Geri Ödənilib",
|
||||
"REFUNDED": "Geri Ödənilib",
|
||||
"VOIDED": "Ləğv Edilib"
|
||||
},
|
||||
"FULFILLMENT_STATUS": {
|
||||
"FULFILLED": "Fulfilled",
|
||||
"PARTIALLY_FULFILLED": "Partially Fulfilled",
|
||||
"UNFULFILLED": "Unfulfilled"
|
||||
"FULFILLED": "Yerinə Yetirilib",
|
||||
"PARTIALLY_FULFILLED": "Qismən Yerinə Yetirilib",
|
||||
"UNFULFILLED": "Yerinə Yetirilməyib"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CONVERSATION_CUSTOM_ATTRIBUTES": {
|
||||
"ADD_BUTTON_TEXT": "Create attribute",
|
||||
"NO_RECORDS_FOUND": "No attributes found",
|
||||
"ADD_BUTTON_TEXT": "Xüsusiyyət yaradın",
|
||||
"NO_RECORDS_FOUND": "Heç bir atribut tapılmadı",
|
||||
"UPDATE": {
|
||||
"SUCCESS": "Attribute updated successfully",
|
||||
"ERROR": "Unable to update attribute. Please try again later"
|
||||
"SUCCESS": "Xüsusiyyət uğurla yeniləndi",
|
||||
"ERROR": "Xüsusiyyət yenilənə bilmədi. Zəhmət olmasa, bir az sonra yenidən cəhd edin"
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Add",
|
||||
"SUCCESS": "Attribute added successfully",
|
||||
"ERROR": "Unable to add attribute. Please try again later"
|
||||
"TITLE": "Əlavə et",
|
||||
"SUCCESS": "Xüsusiyyət uğurla əlavə edildi",
|
||||
"ERROR": "Xüsusiyyət əlavə edilə bilmədi. Zəhmət olmasa, bir az sonra yenidən cəhd edin"
|
||||
},
|
||||
"DELETE": {
|
||||
"SUCCESS": "Attribute deleted successfully",
|
||||
"ERROR": "Unable to delete attribute. Please try again later"
|
||||
"SUCCESS": "Xüsusiyyət uğurla silindi",
|
||||
"ERROR": "Atributu silmək mümkün olmadı. Zəhmət olmasa, bir az sonra yenidən cəhd edin"
|
||||
},
|
||||
"ATTRIBUTE_SELECT": {
|
||||
"TITLE": "Add attributes",
|
||||
"PLACEHOLDER": "Search attributes",
|
||||
"NO_RESULT": "No attributes found"
|
||||
"TITLE": "Atributlar əlavə et",
|
||||
"PLACEHOLDER": "Atributlarda axtar",
|
||||
"NO_RESULT": "Heç bir atribut tapılmadı"
|
||||
}
|
||||
},
|
||||
"EMAIL_HEADER": {
|
||||
"FROM": "From",
|
||||
"TO": "To",
|
||||
"BCC": "Bcc",
|
||||
"CC": "Cc",
|
||||
"SUBJECT": "Subject",
|
||||
"EXPAND": "Expand email"
|
||||
"FROM": "Kimdən",
|
||||
"TO": "Kimə",
|
||||
"BCC": "Gizli nüsxə",
|
||||
"CC": "Nüsxə",
|
||||
"SUBJECT": "Mövzu",
|
||||
"EXPAND": "E-poçtu genişləndir"
|
||||
},
|
||||
"CONVERSATION_PARTICIPANTS": {
|
||||
"SIDEBAR_MENU_TITLE": "Participating",
|
||||
"SIDEBAR_TITLE": "Conversation participants",
|
||||
"NO_RECORDS_FOUND": "No results found",
|
||||
"ADD_PARTICIPANTS": "Select participants",
|
||||
"REMANING_PARTICIPANTS_TEXT": "+{count} others",
|
||||
"REMANING_PARTICIPANT_TEXT": "+{count} other",
|
||||
"TOTAL_PARTICIPANTS_TEXT": "{count} people are participating.",
|
||||
"TOTAL_PARTICIPANT_TEXT": "{count} person is participating.",
|
||||
"SIDEBAR_MENU_TITLE": "İştirak edənlər",
|
||||
"SIDEBAR_TITLE": "Söhbət iştirakçıları",
|
||||
"NO_RECORDS_FOUND": "Nəticə tapılmadı",
|
||||
"ADD_PARTICIPANTS": "İştirakçıları seçin",
|
||||
"REMANING_PARTICIPANTS_TEXT": "+{count} digər",
|
||||
"REMANING_PARTICIPANT_TEXT": "+{count} digər",
|
||||
"TOTAL_PARTICIPANTS_TEXT": "{count} nəfər iştirak edir.",
|
||||
"TOTAL_PARTICIPANT_TEXT": "{count} nəfər iştirak edir.",
|
||||
"NO_PARTICIPANTS_TEXT": "No one is participating!.",
|
||||
"WATCH_CONVERSATION": "Join conversation",
|
||||
"YOU_ARE_WATCHING": "You are participating",
|
||||
"WATCH_CONVERSATION": "Söhbətə qoşulun",
|
||||
"YOU_ARE_WATCHING": "Siz iştirak edirsiniz",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "Could not update, try again!",
|
||||
"SUCCESS_MESSAGE": "Participants updated!"
|
||||
"ERROR_MESSAGE": "Yenilənmədi, yenidən cəhd edin!",
|
||||
"SUCCESS_MESSAGE": "İştirakçılar yeniləndi!"
|
||||
}
|
||||
},
|
||||
"TRANSLATE_MODAL": {
|
||||
"TITLE": "View translated content",
|
||||
"TITLE": "Tərcümə edilmiş məzmunu göstər",
|
||||
"DESC": "You can view the translated content in each langauge.",
|
||||
"ORIGINAL_CONTENT": "Original Content",
|
||||
"TRANSLATED_CONTENT": "Translated Content",
|
||||
"NO_TRANSLATIONS_AVAILABLE": "No translations are available for this content"
|
||||
"ORIGINAL_CONTENT": "Orijinal məzmun",
|
||||
"TRANSLATED_CONTENT": "Tərcümə edilmiş məzmun",
|
||||
"NO_TRANSLATIONS_AVAILABLE": "Bu məzmun üçün tərcümə mövcud deyil"
|
||||
},
|
||||
"TYPING": {
|
||||
"ONE": "{user} is typing",
|
||||
"TWO": "{user} and {secondUser} are typing",
|
||||
"MULTIPLE": "{user} and {count} others are typing"
|
||||
"ONE": "{user} yazır",
|
||||
"TWO": "{user} və {secondUser} yazırlar",
|
||||
"MULTIPLE": "{user} və {count} başqası yazırlar"
|
||||
},
|
||||
"COPILOT": {
|
||||
"TRY_THESE_PROMPTS": "Try these prompts"
|
||||
"TRY_THESE_PROMPTS": "Bu təklifləri sınayın"
|
||||
},
|
||||
"GALLERY_VIEW": {
|
||||
"ERROR_DOWNLOADING": "Unable to download attachment. Please try again"
|
||||
"ERROR_DOWNLOADING": "Əlavəni yükləmək mümkün olmadı. Zəhmət olmasa yenidən cəhd edin"
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user