Compare commits
37
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
040584fc68 | ||
|
|
829142c808 | ||
|
|
3a71829b46 | ||
|
|
0974aea300 | ||
|
|
9fb0dfa4a7 | ||
|
|
8bbb8ba5a4 | ||
|
|
a8aefa0c73 | ||
|
|
b536f35fa7 | ||
|
|
0d721bc898 | ||
|
|
c29a08f0ca | ||
|
|
66cfef9298 | ||
|
|
109aaa2341 | ||
|
|
ecff66146a | ||
|
|
21366e1c3b | ||
|
|
406a470c81 | ||
|
|
6c6aaf573c | ||
|
|
487209574c | ||
|
|
7f1671c083 | ||
|
|
b00261d7c2 | ||
|
|
fcb91ab88a | ||
|
|
59f7c8aa55 | ||
|
|
cd2c58726f | ||
|
|
03d0688cc2 | ||
|
|
b75ea7a762 | ||
|
|
f44e47a624 | ||
|
|
47bdb6d2bb | ||
|
|
c3680d50bc | ||
|
|
2ba4780bda | ||
|
|
d3cd647e49 | ||
|
|
eadbddaa9f | ||
|
|
9f14e6abb6 | ||
|
|
79793a5435 | ||
|
|
e68522318b | ||
|
|
44fab70048 | ||
|
|
68c070bcd9 | ||
|
|
728956a734 | ||
|
|
114c25cae8 |
@@ -103,7 +103,7 @@ gem 'twitty', '~> 0.1.5'
|
|||||||
# facebook client
|
# facebook client
|
||||||
gem 'koala'
|
gem 'koala'
|
||||||
# slack client
|
# slack client
|
||||||
gem 'slack-ruby-client', '~> 2.5.2'
|
gem 'slack-ruby-client', '~> 2.7.0'
|
||||||
# for dialogflow integrations
|
# for dialogflow integrations
|
||||||
gem 'google-cloud-dialogflow-v2', '>= 0.24.0'
|
gem 'google-cloud-dialogflow-v2', '>= 0.24.0'
|
||||||
gem 'grpc'
|
gem 'grpc'
|
||||||
|
|||||||
+4
-4
@@ -292,7 +292,7 @@ GEM
|
|||||||
logger
|
logger
|
||||||
faraday-follow_redirects (0.3.0)
|
faraday-follow_redirects (0.3.0)
|
||||||
faraday (>= 1, < 3)
|
faraday (>= 1, < 3)
|
||||||
faraday-mashify (0.1.1)
|
faraday-mashify (1.0.0)
|
||||||
faraday (~> 2.0)
|
faraday (~> 2.0)
|
||||||
hashie
|
hashie
|
||||||
faraday-multipart (1.0.4)
|
faraday-multipart (1.0.4)
|
||||||
@@ -876,8 +876,8 @@ GEM
|
|||||||
simplecov_json_formatter (~> 0.1)
|
simplecov_json_formatter (~> 0.1)
|
||||||
simplecov-html (0.13.2)
|
simplecov-html (0.13.2)
|
||||||
simplecov_json_formatter (0.1.4)
|
simplecov_json_formatter (0.1.4)
|
||||||
slack-ruby-client (2.5.2)
|
slack-ruby-client (2.7.0)
|
||||||
faraday (>= 2.0)
|
faraday (>= 2.0.1)
|
||||||
faraday-mashify
|
faraday-mashify
|
||||||
faraday-multipart
|
faraday-multipart
|
||||||
gli
|
gli
|
||||||
@@ -1103,7 +1103,7 @@ DEPENDENCIES
|
|||||||
sidekiq_alive
|
sidekiq_alive
|
||||||
simplecov (>= 0.21)
|
simplecov (>= 0.21)
|
||||||
simplecov_json_formatter
|
simplecov_json_formatter
|
||||||
slack-ruby-client (~> 2.5.2)
|
slack-ruby-client (~> 2.7.0)
|
||||||
spring
|
spring
|
||||||
spring-watcher-listen
|
spring-watcher-listen
|
||||||
squasher
|
squasher
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
class Email::BaseBuilder
|
||||||
|
pattr_initialize [:inbox!]
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def channel
|
||||||
|
@channel ||= inbox.channel
|
||||||
|
end
|
||||||
|
|
||||||
|
def account
|
||||||
|
@account ||= inbox.account
|
||||||
|
end
|
||||||
|
|
||||||
|
def conversation
|
||||||
|
@conversation ||= message.conversation
|
||||||
|
end
|
||||||
|
|
||||||
|
def custom_sender_name
|
||||||
|
message&.sender&.available_name || I18n.t('conversations.reply.email.header.notifications')
|
||||||
|
end
|
||||||
|
|
||||||
|
def sender_name(sender_email)
|
||||||
|
# Friendly: <agent_name> from <business_name>
|
||||||
|
# Professional: <business_name>
|
||||||
|
if inbox.friendly?
|
||||||
|
I18n.t(
|
||||||
|
'conversations.reply.email.header.friendly_name',
|
||||||
|
sender_name: custom_sender_name,
|
||||||
|
business_name: business_name,
|
||||||
|
from_email: sender_email
|
||||||
|
)
|
||||||
|
else
|
||||||
|
I18n.t(
|
||||||
|
'conversations.reply.email.header.professional_name',
|
||||||
|
business_name: business_name,
|
||||||
|
from_email: sender_email
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def business_name
|
||||||
|
inbox.business_name || inbox.sanitized_name
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_support_email
|
||||||
|
# Parse the email to ensure it's in the correct format, the user
|
||||||
|
# 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
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
class Email::FromBuilder < Email::BaseBuilder
|
||||||
|
pattr_initialize [:inbox!, :message!]
|
||||||
|
|
||||||
|
def build
|
||||||
|
return sender_name(account_support_email) unless inbox.email?
|
||||||
|
|
||||||
|
from_email = case email_channel_type
|
||||||
|
when :standard_imap_smtp,
|
||||||
|
:google_oauth,
|
||||||
|
:microsoft_oauth,
|
||||||
|
:forwarding_own_smtp
|
||||||
|
channel.email
|
||||||
|
when :imap_chatwoot_smtp,
|
||||||
|
:forwarding_chatwoot_smtp
|
||||||
|
channel.verified_for_sending ? channel.email : account_support_email
|
||||||
|
else
|
||||||
|
account_support_email
|
||||||
|
end
|
||||||
|
|
||||||
|
sender_name(from_email)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def email_channel_type
|
||||||
|
return :google_oauth if channel.google?
|
||||||
|
return :microsoft_oauth if channel.microsoft?
|
||||||
|
return :standard_imap_smtp if imap_and_smtp_enabled?
|
||||||
|
return :imap_chatwoot_smtp if imap_enabled_without_smtp?
|
||||||
|
return :forwarding_own_smtp if forwarding_with_own_smtp?
|
||||||
|
return :forwarding_chatwoot_smtp if forwarding_without_smtp?
|
||||||
|
|
||||||
|
:unknown
|
||||||
|
end
|
||||||
|
|
||||||
|
def imap_and_smtp_enabled?
|
||||||
|
channel.imap_enabled && channel.smtp_enabled
|
||||||
|
end
|
||||||
|
|
||||||
|
def imap_enabled_without_smtp?
|
||||||
|
channel.imap_enabled && !channel.smtp_enabled
|
||||||
|
end
|
||||||
|
|
||||||
|
def forwarding_with_own_smtp?
|
||||||
|
!channel.imap_enabled && channel.smtp_enabled
|
||||||
|
end
|
||||||
|
|
||||||
|
def forwarding_without_smtp?
|
||||||
|
!channel.imap_enabled && !channel.smtp_enabled
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
class Email::ReplyToBuilder < Email::BaseBuilder
|
||||||
|
pattr_initialize [:inbox!, :message!]
|
||||||
|
|
||||||
|
def build
|
||||||
|
reply_to = if inbox.email?
|
||||||
|
channel.email
|
||||||
|
elsif inbound_email_enabled?
|
||||||
|
"reply+#{conversation.uuid}@#{account.inbound_email_domain}"
|
||||||
|
else
|
||||||
|
account_support_email
|
||||||
|
end
|
||||||
|
|
||||||
|
sender_name(reply_to)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def inbound_email_enabled?
|
||||||
|
account.feature_enabled?('inbound_emails') && account.inbound_email_domain.present?
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -7,6 +7,7 @@ class Messages::MessageBuilder
|
|||||||
@private = params[:private] || false
|
@private = params[:private] || false
|
||||||
@conversation = conversation
|
@conversation = conversation
|
||||||
@user = user
|
@user = user
|
||||||
|
@account = conversation.account
|
||||||
@message_type = params[:message_type] || 'outgoing'
|
@message_type = params[:message_type] || 'outgoing'
|
||||||
@attachments = params[:attachments]
|
@attachments = params[:attachments]
|
||||||
@automation_rule = content_attributes&.dig(:automation_rule_id)
|
@automation_rule = content_attributes&.dig(:automation_rule_id)
|
||||||
@@ -20,6 +21,9 @@ class Messages::MessageBuilder
|
|||||||
@message = @conversation.messages.build(message_params)
|
@message = @conversation.messages.build(message_params)
|
||||||
process_attachments
|
process_attachments
|
||||||
process_emails
|
process_emails
|
||||||
|
# When the message has no quoted content, it will just be rendered as a regular message
|
||||||
|
# The frontend is equipped to handle this case
|
||||||
|
process_email_content if @account.feature_enabled?(:quoted_email_reply)
|
||||||
@message.save!
|
@message.save!
|
||||||
@message
|
@message
|
||||||
end
|
end
|
||||||
@@ -92,6 +96,14 @@ class Messages::MessageBuilder
|
|||||||
@message.content_attributes[:to_emails] = to_emails
|
@message.content_attributes[:to_emails] = to_emails
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def process_email_content
|
||||||
|
return unless should_process_email_content?
|
||||||
|
|
||||||
|
@message.content_attributes ||= {}
|
||||||
|
email_attributes = build_email_attributes
|
||||||
|
@message.content_attributes[:email] = email_attributes
|
||||||
|
end
|
||||||
|
|
||||||
def process_email_string(email_string)
|
def process_email_string(email_string)
|
||||||
return [] if email_string.blank?
|
return [] if email_string.blank?
|
||||||
|
|
||||||
@@ -153,4 +165,52 @@ class Messages::MessageBuilder
|
|||||||
source_id: @params[:source_id]
|
source_id: @params[:source_id]
|
||||||
}.merge(external_created_at).merge(automation_rule_id).merge(campaign_id).merge(template_params)
|
}.merge(external_created_at).merge(automation_rule_id).merge(campaign_id).merge(template_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def email_inbox?
|
||||||
|
@conversation.inbox&.inbox_type == 'Email'
|
||||||
|
end
|
||||||
|
|
||||||
|
def should_process_email_content?
|
||||||
|
email_inbox? && !@private && @message.content.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_email_attributes
|
||||||
|
email_attributes = ensure_indifferent_access(@message.content_attributes[:email] || {})
|
||||||
|
normalized_content = normalize_email_body(@message.content)
|
||||||
|
|
||||||
|
email_attributes[:html_content] = build_html_content(normalized_content)
|
||||||
|
email_attributes[:text_content] = build_text_content(normalized_content)
|
||||||
|
email_attributes
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_html_content(normalized_content)
|
||||||
|
html_content = ensure_indifferent_access(@message.content_attributes.dig(:email, :html_content) || {})
|
||||||
|
rendered_html = render_email_html(normalized_content)
|
||||||
|
html_content[:full] = rendered_html
|
||||||
|
html_content[:reply] = rendered_html
|
||||||
|
html_content
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_text_content(normalized_content)
|
||||||
|
text_content = ensure_indifferent_access(@message.content_attributes.dig(:email, :text_content) || {})
|
||||||
|
text_content[:full] = normalized_content
|
||||||
|
text_content[:reply] = normalized_content
|
||||||
|
text_content
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_indifferent_access(hash)
|
||||||
|
return {} if hash.blank?
|
||||||
|
|
||||||
|
hash.respond_to?(:with_indifferent_access) ? hash.with_indifferent_access : hash
|
||||||
|
end
|
||||||
|
|
||||||
|
def normalize_email_body(content)
|
||||||
|
content.to_s.gsub("\r\n", "\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_email_html(content)
|
||||||
|
return '' if content.blank?
|
||||||
|
|
||||||
|
ChatwootMarkdownRenderer.new(content).render_message.to_s
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||||||
before_action :set_include_contact_inboxes, only: [:index, :active, :search, :filter, :show, :update]
|
before_action :set_include_contact_inboxes, only: [:index, :active, :search, :filter, :show, :update]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@contacts_count = resolved_contacts.count
|
|
||||||
@contacts = fetch_contacts(resolved_contacts)
|
@contacts = fetch_contacts(resolved_contacts)
|
||||||
|
@contacts_count = @contacts.total_count
|
||||||
end
|
end
|
||||||
|
|
||||||
def search
|
def search
|
||||||
@@ -29,8 +29,8 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||||||
OR contacts.additional_attributes->>\'company_name\' ILIKE :search',
|
OR contacts.additional_attributes->>\'company_name\' ILIKE :search',
|
||||||
search: "%#{params[:q].strip}%"
|
search: "%#{params[:q].strip}%"
|
||||||
)
|
)
|
||||||
@contacts_count = contacts.count
|
|
||||||
@contacts = fetch_contacts(contacts)
|
@contacts = fetch_contacts(contacts)
|
||||||
|
@contacts_count = @contacts.total_count
|
||||||
end
|
end
|
||||||
|
|
||||||
def import
|
def import
|
||||||
@@ -55,8 +55,8 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||||||
def active
|
def active
|
||||||
contacts = Current.account.contacts.where(id: ::OnlineStatusTracker
|
contacts = Current.account.contacts.where(id: ::OnlineStatusTracker
|
||||||
.get_available_contact_ids(Current.account.id))
|
.get_available_contact_ids(Current.account.id))
|
||||||
@contacts_count = contacts.count
|
|
||||||
@contacts = fetch_contacts(contacts)
|
@contacts = fetch_contacts(contacts)
|
||||||
|
@contacts_count = @contacts.total_count
|
||||||
end
|
end
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
@@ -133,13 +133,14 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def fetch_contacts(contacts)
|
def fetch_contacts(contacts)
|
||||||
contacts_with_avatar = filtrate(contacts)
|
# Build includes hash to avoid separate query when contact_inboxes are needed
|
||||||
.includes([{ avatar_attachment: [:blob] }])
|
includes_hash = { avatar_attachment: [:blob] }
|
||||||
.page(@current_page).per(RESULTS_PER_PAGE)
|
includes_hash[:contact_inboxes] = { inbox: :channel } if @include_contact_inboxes
|
||||||
|
|
||||||
return contacts_with_avatar.includes([{ contact_inboxes: [:inbox] }]) if @include_contact_inboxes
|
filtrate(contacts)
|
||||||
|
.includes(includes_hash)
|
||||||
contacts_with_avatar
|
.page(@current_page)
|
||||||
|
.per(RESULTS_PER_PAGE)
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_contact_inbox
|
def build_contact_inbox
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
|||||||
before_action :fetch_agent_bot, only: [:set_agent_bot]
|
before_action :fetch_agent_bot, only: [:set_agent_bot]
|
||||||
before_action :validate_limit, only: [:create]
|
before_action :validate_limit, only: [:create]
|
||||||
# we are already handling the authorization in fetch inbox
|
# we are already handling the authorization in fetch inbox
|
||||||
before_action :check_authorization, except: [:show]
|
before_action :check_authorization, except: [:show, :health]
|
||||||
|
before_action :validate_whatsapp_cloud_channel, only: [:health]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@inboxes = policy_scope(Current.account.inboxes.order_by_name.includes(:channel, { avatar_attachment: [:blob] }))
|
@inboxes = policy_scope(Current.account.inboxes.order_by_name.includes(:channel, { avatar_attachment: [:blob] }))
|
||||||
@@ -78,6 +79,14 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
|||||||
render status: :internal_server_error, json: { error: e.message }
|
render status: :internal_server_error, json: { error: e.message }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def health
|
||||||
|
health_data = Whatsapp::HealthService.new(@inbox.channel).fetch_health_status
|
||||||
|
render json: health_data
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger.error "[INBOX HEALTH] Error fetching health data: #{e.message}"
|
||||||
|
render json: { error: e.message }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def fetch_inbox
|
def fetch_inbox
|
||||||
@@ -89,6 +98,12 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
|||||||
@agent_bot = AgentBot.find(params[:agent_bot]) if params[:agent_bot]
|
@agent_bot = AgentBot.find(params[:agent_bot]) if params[:agent_bot]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def validate_whatsapp_cloud_channel
|
||||||
|
return if @inbox.channel.is_a?(Channel::Whatsapp) && @inbox.channel.provider == 'whatsapp_cloud'
|
||||||
|
|
||||||
|
render json: { error: 'Health data only available for WhatsApp Cloud API channels' }, status: :bad_request
|
||||||
|
end
|
||||||
|
|
||||||
def create_channel
|
def create_channel
|
||||||
return unless allowed_channel_types.include?(permitted_params[:channel][:type])
|
return unless allowed_channel_types.include?(permitted_params[:channel][:type])
|
||||||
|
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ class SuperAdmin::UsersController < SuperAdmin::ApplicationController
|
|||||||
redirect_to new_super_admin_user_path, notice: notice
|
redirect_to new_super_admin_user_path, notice: notice
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
#
|
|
||||||
# def update
|
def update
|
||||||
# super
|
requested_resource.skip_reconfirmation! if resource_params[:confirmed_at].present?
|
||||||
# send_foo_updated_email(requested_resource)
|
super
|
||||||
# end
|
end
|
||||||
|
|
||||||
# Override this method to specify custom lookup behavior.
|
# Override this method to specify custom lookup behavior.
|
||||||
# This will be used to set the resource for the `show`, `edit`, and `update`
|
# This will be used to set the resource for the `show`, `edit`, and `update`
|
||||||
|
|||||||
@@ -59,11 +59,11 @@ class UserDashboard < Administrate::BaseDashboard
|
|||||||
SHOW_PAGE_ATTRIBUTES = %i[
|
SHOW_PAGE_ATTRIBUTES = %i[
|
||||||
id
|
id
|
||||||
avatar_url
|
avatar_url
|
||||||
unconfirmed_email
|
|
||||||
name
|
name
|
||||||
type
|
type
|
||||||
display_name
|
display_name
|
||||||
email
|
email
|
||||||
|
unconfirmed_email
|
||||||
created_at
|
created_at
|
||||||
updated_at
|
updated_at
|
||||||
confirmed_at
|
confirmed_at
|
||||||
|
|||||||
@@ -156,7 +156,8 @@ class ConversationFinder
|
|||||||
def filter_by_labels
|
def filter_by_labels
|
||||||
return unless params[:labels]
|
return unless params[:labels]
|
||||||
|
|
||||||
@conversations = @conversations.tagged_with(params[:labels], any: true)
|
pattern = params[:labels].map { |label| Regexp.escape(label) }.join('|')
|
||||||
|
@conversations = @conversations.where('cached_label_list ~ ?', "\\y(#{pattern})\\y")
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_by_source_id
|
def filter_by_source_id
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/* global axios */
|
||||||
|
import ApiClient from '../ApiClient';
|
||||||
|
|
||||||
|
class CaptainCustomTools extends ApiClient {
|
||||||
|
constructor() {
|
||||||
|
super('captain/custom_tools', { accountScoped: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
get({ page = 1, searchKey } = {}) {
|
||||||
|
return axios.get(this.url, {
|
||||||
|
params: { page, searchKey },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
show(id) {
|
||||||
|
return axios.get(`${this.url}/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
create(data = {}) {
|
||||||
|
return axios.post(this.url, {
|
||||||
|
custom_tool: data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
update(id, data = {}) {
|
||||||
|
return axios.put(`${this.url}/${id}`, {
|
||||||
|
custom_tool: data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(id) {
|
||||||
|
return axios.delete(`${this.url}/${id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new CaptainCustomTools();
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/* global axios */
|
||||||
|
import ApiClient from './ApiClient';
|
||||||
|
|
||||||
|
class InboxHealthAPI extends ApiClient {
|
||||||
|
constructor() {
|
||||||
|
super('inboxes', { accountScoped: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
getHealthStatus(inboxId) {
|
||||||
|
return axios.get(`${this.url}/${inboxId}/health`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new InboxHealthAPI();
|
||||||
+14
-3
@@ -148,10 +148,21 @@ const isAnyDropdownActive = computed(() => {
|
|||||||
|
|
||||||
const handleContactSearch = value => {
|
const handleContactSearch = value => {
|
||||||
showContactsDropdown.value = true;
|
showContactsDropdown.value = true;
|
||||||
emit('searchContacts', {
|
const query = typeof value === 'string' ? value.trim() : '';
|
||||||
keys: ['email', 'phone_number', 'name'],
|
const hasAlphabet = Array.from(query).some(char => {
|
||||||
query: value,
|
const lower = char.toLowerCase();
|
||||||
|
const upper = char.toUpperCase();
|
||||||
|
return lower !== upper;
|
||||||
});
|
});
|
||||||
|
const isEmailLike = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(query);
|
||||||
|
|
||||||
|
const keys = ['email', 'phone_number', 'name'].filter(key => {
|
||||||
|
if (key === 'phone_number' && hasAlphabet) return false;
|
||||||
|
if (key === 'name' && isEmailLike) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
emit('searchContacts', { keys, query: value });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDropdownUpdate = (type, value) => {
|
const handleDropdownUpdate = (type, value) => {
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
translationKey: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
entity: {
|
entity: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
@@ -25,7 +29,9 @@ const emit = defineEmits(['deleteSuccess']);
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const deleteDialogRef = ref(null);
|
const deleteDialogRef = ref(null);
|
||||||
const i18nKey = computed(() => props.type.toUpperCase());
|
const i18nKey = computed(() => {
|
||||||
|
return props.translationKey || props.type.toUpperCase();
|
||||||
|
});
|
||||||
|
|
||||||
const deleteEntity = async payload => {
|
const deleteEntity = async payload => {
|
||||||
if (!payload) return;
|
if (!payload) return;
|
||||||
|
|||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
<script setup>
|
||||||
|
import { defineModel, watch } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import Input from 'dashboard/components-next/input/Input.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
authType: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
validator: value => ['none', 'bearer', 'basic', 'api_key'].includes(value),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const authConfig = defineModel('authConfig', {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.authType,
|
||||||
|
() => {
|
||||||
|
authConfig.value = {};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<Input
|
||||||
|
v-if="authType === 'bearer'"
|
||||||
|
v-model="authConfig.token"
|
||||||
|
:label="t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_CONFIG.BEARER_TOKEN')"
|
||||||
|
:placeholder="
|
||||||
|
t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_CONFIG.BEARER_TOKEN_PLACEHOLDER')
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<template v-else-if="authType === 'basic'">
|
||||||
|
<Input
|
||||||
|
v-model="authConfig.username"
|
||||||
|
:label="t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_CONFIG.USERNAME')"
|
||||||
|
:placeholder="
|
||||||
|
t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_CONFIG.USERNAME_PLACEHOLDER')
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
v-model="authConfig.password"
|
||||||
|
type="password"
|
||||||
|
:label="t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_CONFIG.PASSWORD')"
|
||||||
|
:placeholder="
|
||||||
|
t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_CONFIG.PASSWORD_PLACEHOLDER')
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="authType === 'api_key'">
|
||||||
|
<Input
|
||||||
|
v-model="authConfig.name"
|
||||||
|
:label="t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_CONFIG.API_KEY')"
|
||||||
|
:placeholder="
|
||||||
|
t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_CONFIG.API_KEY_PLACEHOLDER')
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
v-model="authConfig.key"
|
||||||
|
:label="t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_CONFIG.API_VALUE')"
|
||||||
|
:placeholder="
|
||||||
|
t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_CONFIG.API_VALUE_PLACEHOLDER')
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { useStore } from 'dashboard/composables/store';
|
||||||
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { parseAPIErrorResponse } from 'dashboard/store/utils/api';
|
||||||
|
|
||||||
|
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||||
|
import CustomToolForm from './CustomToolForm.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
selectedTool: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'create',
|
||||||
|
validator: value => ['create', 'edit'].includes(value),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['close']);
|
||||||
|
const { t } = useI18n();
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
|
const dialogRef = ref(null);
|
||||||
|
|
||||||
|
const updateTool = toolDetails =>
|
||||||
|
store.dispatch('captainCustomTools/update', {
|
||||||
|
id: props.selectedTool.id,
|
||||||
|
...toolDetails,
|
||||||
|
});
|
||||||
|
|
||||||
|
const i18nKey = computed(
|
||||||
|
() => `CAPTAIN.CUSTOM_TOOLS.${props.type.toUpperCase()}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const createTool = toolDetails =>
|
||||||
|
store.dispatch('captainCustomTools/create', toolDetails);
|
||||||
|
|
||||||
|
const handleSubmit = async updatedTool => {
|
||||||
|
try {
|
||||||
|
if (props.type === 'edit') {
|
||||||
|
await updateTool(updatedTool);
|
||||||
|
} else {
|
||||||
|
await createTool(updatedTool);
|
||||||
|
}
|
||||||
|
useAlert(t(`${i18nKey.value}.SUCCESS_MESSAGE`));
|
||||||
|
dialogRef.value.close();
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
parseAPIErrorResponse(error) || t(`${i18nKey.value}.ERROR_MESSAGE`);
|
||||||
|
useAlert(errorMessage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
dialogRef.value.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ dialogRef });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
ref="dialogRef"
|
||||||
|
width="2xl"
|
||||||
|
:title="$t(`${i18nKey}.TITLE`)"
|
||||||
|
:description="$t('CAPTAIN.CUSTOM_TOOLS.FORM_DESCRIPTION')"
|
||||||
|
:show-cancel-button="false"
|
||||||
|
:show-confirm-button="false"
|
||||||
|
@close="handleClose"
|
||||||
|
>
|
||||||
|
<CustomToolForm
|
||||||
|
:mode="type"
|
||||||
|
:tool="selectedTool"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
/>
|
||||||
|
<template #footer />
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
+125
@@ -0,0 +1,125 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useToggle } from '@vueuse/core';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||||
|
|
||||||
|
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 Policy from 'dashboard/components/policy.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
id: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
authType: {
|
||||||
|
type: String,
|
||||||
|
default: 'none',
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['action']);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const [showActionsDropdown, toggleDropdown] = useToggle();
|
||||||
|
|
||||||
|
const menuItems = computed(() => [
|
||||||
|
{
|
||||||
|
label: t('CAPTAIN.CUSTOM_TOOLS.OPTIONS.EDIT_TOOL'),
|
||||||
|
value: 'edit',
|
||||||
|
action: 'edit',
|
||||||
|
icon: 'i-lucide-pencil-line',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('CAPTAIN.CUSTOM_TOOLS.OPTIONS.DELETE_TOOL'),
|
||||||
|
value: 'delete',
|
||||||
|
action: 'delete',
|
||||||
|
icon: 'i-lucide-trash',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const timestamp = computed(() =>
|
||||||
|
dynamicTime(props.updatedAt || props.createdAt)
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleAction = ({ action, value }) => {
|
||||||
|
toggleDropdown(false);
|
||||||
|
emit('action', { action, value, id: props.id });
|
||||||
|
};
|
||||||
|
|
||||||
|
const authTypeLabel = computed(() => {
|
||||||
|
return t(
|
||||||
|
`CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_TYPES.${props.authType.toUpperCase()}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CardLayout class="relative">
|
||||||
|
<div class="flex relative justify-between w-full gap-1">
|
||||||
|
<span class="text-base text-n-slate-12 line-clamp-1 font-medium">
|
||||||
|
{{ title }}
|
||||||
|
</span>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Policy
|
||||||
|
v-on-clickaway="() => toggleDropdown(false)"
|
||||||
|
:permissions="['administrator']"
|
||||||
|
class="relative flex items-center group"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
icon="i-lucide-ellipsis-vertical"
|
||||||
|
color="slate"
|
||||||
|
size="xs"
|
||||||
|
class="rounded-md group-hover:bg-n-alpha-2"
|
||||||
|
@click="toggleDropdown()"
|
||||||
|
/>
|
||||||
|
<DropdownMenu
|
||||||
|
v-if="showActionsDropdown"
|
||||||
|
:menu-items="menuItems"
|
||||||
|
class="mt-1 ltr:right-0 rtl:right-0 top-full"
|
||||||
|
@action="handleAction($event)"
|
||||||
|
/>
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
{{ description }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="authType !== 'none'"
|
||||||
|
class="text-sm shrink-0 text-n-slate-11 inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<i class="i-lucide-lock text-base" />
|
||||||
|
{{ authTypeLabel }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-sm text-n-slate-11 line-clamp-1 shrink-0">
|
||||||
|
{{ timestamp }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</CardLayout>
|
||||||
|
</template>
|
||||||
+271
@@ -0,0 +1,271 @@
|
|||||||
|
<script setup>
|
||||||
|
import { reactive, computed, useTemplateRef, watch } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useVuelidate } from '@vuelidate/core';
|
||||||
|
import { required } from '@vuelidate/validators';
|
||||||
|
import { useMapGetter } from 'dashboard/composables/store';
|
||||||
|
|
||||||
|
import Input from 'dashboard/components-next/input/Input.vue';
|
||||||
|
import TextArea from 'dashboard/components-next/textarea/TextArea.vue';
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue';
|
||||||
|
import ParamRow from './ParamRow.vue';
|
||||||
|
import AuthConfig from './AuthConfig.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: 'create',
|
||||||
|
validator: value => ['create', 'edit'].includes(value),
|
||||||
|
},
|
||||||
|
tool: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['submit', 'cancel']);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const formState = {
|
||||||
|
uiFlags: useMapGetter('captainCustomTools/getUIFlags'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
endpoint_url: '',
|
||||||
|
http_method: 'GET',
|
||||||
|
request_template: '',
|
||||||
|
response_template: '',
|
||||||
|
auth_type: 'none',
|
||||||
|
auth_config: {},
|
||||||
|
param_schema: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = reactive({ ...initialState });
|
||||||
|
|
||||||
|
// Populate form when in edit mode
|
||||||
|
watch(
|
||||||
|
() => props.tool,
|
||||||
|
newTool => {
|
||||||
|
if (props.mode === 'edit' && newTool && newTool.id) {
|
||||||
|
state.title = newTool.title || '';
|
||||||
|
state.description = newTool.description || '';
|
||||||
|
state.endpoint_url = newTool.endpoint_url || '';
|
||||||
|
state.http_method = newTool.http_method || 'GET';
|
||||||
|
state.request_template = newTool.request_template || '';
|
||||||
|
state.response_template = newTool.response_template || '';
|
||||||
|
state.auth_type = newTool.auth_type || 'none';
|
||||||
|
state.auth_config = newTool.auth_config || {};
|
||||||
|
state.param_schema = newTool.param_schema || [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const DEFAULT_PARAM = {
|
||||||
|
name: '',
|
||||||
|
type: 'string',
|
||||||
|
description: '',
|
||||||
|
required: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const validationRules = {
|
||||||
|
title: { required },
|
||||||
|
endpoint_url: { required },
|
||||||
|
http_method: { required },
|
||||||
|
auth_type: { required },
|
||||||
|
};
|
||||||
|
|
||||||
|
const httpMethodOptions = computed(() => [
|
||||||
|
{ value: 'GET', label: 'GET' },
|
||||||
|
{ value: 'POST', label: 'POST' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authTypeOptions = computed(() => [
|
||||||
|
{ value: 'none', label: t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_TYPES.NONE') },
|
||||||
|
{ value: 'bearer', label: t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_TYPES.BEARER') },
|
||||||
|
{ value: 'basic', label: t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_TYPES.BASIC') },
|
||||||
|
{
|
||||||
|
value: 'api_key',
|
||||||
|
label: t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_TYPES.API_KEY'),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const v$ = useVuelidate(validationRules, state);
|
||||||
|
|
||||||
|
const isLoading = computed(() =>
|
||||||
|
props.mode === 'edit'
|
||||||
|
? formState.uiFlags.value.updatingItem
|
||||||
|
: formState.uiFlags.value.creatingItem
|
||||||
|
);
|
||||||
|
|
||||||
|
const getErrorMessage = (field, errorKey) => {
|
||||||
|
return v$.value[field].$error
|
||||||
|
? t(`CAPTAIN.CUSTOM_TOOLS.FORM.${errorKey}.ERROR`)
|
||||||
|
: '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const formErrors = computed(() => ({
|
||||||
|
title: getErrorMessage('title', 'TITLE'),
|
||||||
|
endpoint_url: getErrorMessage('endpoint_url', 'ENDPOINT_URL'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const paramsRef = useTemplateRef('paramsRef');
|
||||||
|
|
||||||
|
const isParamsValid = () => {
|
||||||
|
if (!paramsRef.value || paramsRef.value.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return paramsRef.value.every(param => param.validate());
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeParam = index => {
|
||||||
|
state.param_schema.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addParam = () => {
|
||||||
|
state.param_schema.push({ ...DEFAULT_PARAM });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => emit('cancel');
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
const isFormValid = await v$.value.$validate();
|
||||||
|
if (!isFormValid || !isParamsValid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('submit', state);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form
|
||||||
|
class="flex flex-col px-4 -mx-4 gap-4 max-h-[calc(100vh-200px)] overflow-y-scroll"
|
||||||
|
@submit.prevent="handleSubmit"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
v-model="state.title"
|
||||||
|
:label="t('CAPTAIN.CUSTOM_TOOLS.FORM.TITLE.LABEL')"
|
||||||
|
:placeholder="t('CAPTAIN.CUSTOM_TOOLS.FORM.TITLE.PLACEHOLDER')"
|
||||||
|
:message="formErrors.title"
|
||||||
|
:message-type="formErrors.title ? 'error' : 'info'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextArea
|
||||||
|
v-model="state.description"
|
||||||
|
:label="t('CAPTAIN.CUSTOM_TOOLS.FORM.DESCRIPTION.LABEL')"
|
||||||
|
:placeholder="t('CAPTAIN.CUSTOM_TOOLS.FORM.DESCRIPTION.PLACEHOLDER')"
|
||||||
|
:rows="2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<div class="flex flex-col gap-1 w-28">
|
||||||
|
<label class="mb-0.5 text-sm font-medium text-n-slate-12">
|
||||||
|
{{ t('CAPTAIN.CUSTOM_TOOLS.FORM.HTTP_METHOD.LABEL') }}
|
||||||
|
</label>
|
||||||
|
<ComboBox
|
||||||
|
v-model="state.http_method"
|
||||||
|
:options="httpMethodOptions"
|
||||||
|
class="[&>div>button]:bg-n-alpha-black2 [&_li]:font-mono [&_button]:font-mono [&>div>button]:outline-offset-[-1px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
v-model="state.endpoint_url"
|
||||||
|
:label="t('CAPTAIN.CUSTOM_TOOLS.FORM.ENDPOINT_URL.LABEL')"
|
||||||
|
:placeholder="t('CAPTAIN.CUSTOM_TOOLS.FORM.ENDPOINT_URL.PLACEHOLDER')"
|
||||||
|
:message="formErrors.endpoint_url"
|
||||||
|
:message-type="formErrors.endpoint_url ? 'error' : 'info'"
|
||||||
|
class="flex-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label class="mb-0.5 text-sm font-medium text-n-slate-12">
|
||||||
|
{{ t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_TYPE.LABEL') }}
|
||||||
|
</label>
|
||||||
|
<ComboBox
|
||||||
|
v-model="state.auth_type"
|
||||||
|
:options="authTypeOptions"
|
||||||
|
class="[&>div>button]:bg-n-alpha-black2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AuthConfig
|
||||||
|
v-model:auth-config="state.auth_config"
|
||||||
|
:auth-type="state.auth_type"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-sm font-medium text-n-slate-12">
|
||||||
|
{{ t('CAPTAIN.CUSTOM_TOOLS.FORM.PARAMETERS.LABEL') }}
|
||||||
|
</label>
|
||||||
|
<p class="text-xs text-n-slate-11 -mt-1">
|
||||||
|
{{ t('CAPTAIN.CUSTOM_TOOLS.FORM.PARAMETERS.HELP_TEXT') }}
|
||||||
|
</p>
|
||||||
|
<ul v-if="state.param_schema.length > 0" class="grid gap-2 list-none">
|
||||||
|
<ParamRow
|
||||||
|
v-for="(param, index) in state.param_schema"
|
||||||
|
:key="index"
|
||||||
|
ref="paramsRef"
|
||||||
|
v-model:name="param.name"
|
||||||
|
v-model:type="param.type"
|
||||||
|
v-model:description="param.description"
|
||||||
|
v-model:required="param.required"
|
||||||
|
@remove="removeParam(index)"
|
||||||
|
/>
|
||||||
|
</ul>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
sm
|
||||||
|
ghost
|
||||||
|
blue
|
||||||
|
icon="i-lucide-plus"
|
||||||
|
:label="t('CAPTAIN.CUSTOM_TOOLS.FORM.ADD_PARAMETER')"
|
||||||
|
@click="addParam"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TextArea
|
||||||
|
v-if="state.http_method === 'POST'"
|
||||||
|
v-model="state.request_template"
|
||||||
|
:label="t('CAPTAIN.CUSTOM_TOOLS.FORM.REQUEST_TEMPLATE.LABEL')"
|
||||||
|
:placeholder="t('CAPTAIN.CUSTOM_TOOLS.FORM.REQUEST_TEMPLATE.PLACEHOLDER')"
|
||||||
|
:rows="4"
|
||||||
|
class="[&_textarea]:font-mono"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextArea
|
||||||
|
v-model="state.response_template"
|
||||||
|
:label="t('CAPTAIN.CUSTOM_TOOLS.FORM.RESPONSE_TEMPLATE.LABEL')"
|
||||||
|
:placeholder="
|
||||||
|
t('CAPTAIN.CUSTOM_TOOLS.FORM.RESPONSE_TEMPLATE.PLACEHOLDER')
|
||||||
|
"
|
||||||
|
:rows="4"
|
||||||
|
class="[&_textarea]:font-mono"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex gap-3 justify-between items-center w-full">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="faded"
|
||||||
|
color="slate"
|
||||||
|
:label="t('CAPTAIN.FORM.CANCEL')"
|
||||||
|
class="w-full bg-n-alpha-2 text-n-blue-text hover:bg-n-alpha-3"
|
||||||
|
@click="handleCancel"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
:label="
|
||||||
|
t(mode === 'edit' ? 'CAPTAIN.FORM.EDIT' : 'CAPTAIN.FORM.CREATE')
|
||||||
|
"
|
||||||
|
class="w-full"
|
||||||
|
:is-loading="isLoading"
|
||||||
|
:disabled="isLoading"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, defineModel, ref, watch } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import Input from 'dashboard/components-next/input/Input.vue';
|
||||||
|
import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue';
|
||||||
|
import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue';
|
||||||
|
|
||||||
|
const emit = defineEmits(['remove']);
|
||||||
|
const { t } = useI18n();
|
||||||
|
const showErrors = ref(false);
|
||||||
|
|
||||||
|
const name = defineModel('name', {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const type = defineModel('type', {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const description = defineModel('description', {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const required = defineModel('required', {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const paramTypeOptions = computed(() => [
|
||||||
|
{ value: 'string', label: t('CAPTAIN.CUSTOM_TOOLS.FORM.PARAM_TYPES.STRING') },
|
||||||
|
{ value: 'number', label: t('CAPTAIN.CUSTOM_TOOLS.FORM.PARAM_TYPES.NUMBER') },
|
||||||
|
{
|
||||||
|
value: 'boolean',
|
||||||
|
label: t('CAPTAIN.CUSTOM_TOOLS.FORM.PARAM_TYPES.BOOLEAN'),
|
||||||
|
},
|
||||||
|
{ value: 'array', label: t('CAPTAIN.CUSTOM_TOOLS.FORM.PARAM_TYPES.ARRAY') },
|
||||||
|
{ value: 'object', label: t('CAPTAIN.CUSTOM_TOOLS.FORM.PARAM_TYPES.OBJECT') },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const validationError = computed(() => {
|
||||||
|
if (!name.value || name.value.trim() === '') {
|
||||||
|
return 'PARAM_NAME_REQUIRED';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch([name, type, description, required], () => {
|
||||||
|
showErrors.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const validate = () => {
|
||||||
|
showErrors.value = true;
|
||||||
|
return !validationError.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ validate });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<li class="list-none">
|
||||||
|
<div
|
||||||
|
class="flex items-start gap-2 p-3 rounded-lg border border-n-weak bg-n-alpha-2"
|
||||||
|
:class="{
|
||||||
|
'animate-wiggle border-n-ruby-9': showErrors && validationError,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col flex-1 gap-3">
|
||||||
|
<div class="grid grid-cols-3 gap-2">
|
||||||
|
<Input
|
||||||
|
v-model="name"
|
||||||
|
:placeholder="t('CAPTAIN.CUSTOM_TOOLS.FORM.PARAM_NAME.PLACEHOLDER')"
|
||||||
|
class="col-span-2"
|
||||||
|
/>
|
||||||
|
<ComboBox
|
||||||
|
v-model="type"
|
||||||
|
:options="paramTypeOptions"
|
||||||
|
:placeholder="t('CAPTAIN.CUSTOM_TOOLS.FORM.PARAM_TYPE.PLACEHOLDER')"
|
||||||
|
class="[&>div>button]:bg-n-alpha-black2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
v-model="description"
|
||||||
|
:placeholder="
|
||||||
|
t('CAPTAIN.CUSTOM_TOOLS.FORM.PARAM_DESCRIPTION.PLACEHOLDER')
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<Checkbox v-model="required" />
|
||||||
|
<span class="text-sm text-n-slate-11">
|
||||||
|
{{ t('CAPTAIN.CUSTOM_TOOLS.FORM.PARAM_REQUIRED.LABEL') }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
solid
|
||||||
|
slate
|
||||||
|
icon="i-lucide-trash"
|
||||||
|
class="flex-shrink-0"
|
||||||
|
@click.stop="emit('remove')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
v-if="showErrors && validationError"
|
||||||
|
class="block mt-1 text-sm text-n-ruby-11"
|
||||||
|
>
|
||||||
|
{{ t(`CAPTAIN.CUSTOM_TOOLS.FORM.ERRORS.${validationError}`) }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
<script setup>
|
||||||
|
import EmptyStateLayout from 'dashboard/components-next/EmptyStateLayout.vue';
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
|
||||||
|
const emit = defineEmits(['click']);
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
emit('click');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<EmptyStateLayout
|
||||||
|
:title="$t('CAPTAIN.CUSTOM_TOOLS.EMPTY_STATE.TITLE')"
|
||||||
|
:subtitle="$t('CAPTAIN.CUSTOM_TOOLS.EMPTY_STATE.SUBTITLE')"
|
||||||
|
:action-perms="['administrator']"
|
||||||
|
>
|
||||||
|
<template #empty-state-item>
|
||||||
|
<div class="min-h-[600px]" />
|
||||||
|
</template>
|
||||||
|
<template #actions>
|
||||||
|
<Button
|
||||||
|
:label="$t('CAPTAIN.CUSTOM_TOOLS.ADD_NEW')"
|
||||||
|
icon="i-lucide-plus"
|
||||||
|
@click="onClick"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</EmptyStateLayout>
|
||||||
|
</template>
|
||||||
@@ -45,7 +45,7 @@ const activeAssistantLabel = computed(() => {
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<DropdownBody class="bottom-9 min-w-64 z-50" strong>
|
<DropdownBody class="bottom-9 min-w-64 z-50" strong>
|
||||||
<DropdownSection class="max-h-80 overflow-scroll">
|
<DropdownSection class="[&>ul]:max-h-80">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
v-for="assistant in assistants"
|
v-for="assistant in assistants"
|
||||||
:key="assistant.id"
|
:key="assistant.id"
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ const updateSelected = newValue => {
|
|||||||
:class="dropdownPosition"
|
:class="dropdownPosition"
|
||||||
strong
|
strong
|
||||||
>
|
>
|
||||||
<DropdownSection class="max-h-80 overflow-scroll">
|
<DropdownSection class="[&>ul]:max-h-80">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
v-for="option in options"
|
v-for="option in options"
|
||||||
:key="option.value"
|
:key="option.value"
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ const toggleOption = option => {
|
|||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
<DropdownBody class="top-0 min-w-48 z-50" strong>
|
<DropdownBody class="top-0 min-w-48 z-50" strong>
|
||||||
<DropdownSection class="max-h-80 overflow-scroll">
|
<DropdownSection class="[&>ul]:max-h-80">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
v-for="option in options"
|
v-for="option in options"
|
||||||
:key="option.id"
|
:key="option.id"
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ const toggleSelected = option => {
|
|||||||
:placeholder="searchPlaceholder || t('COMBOBOX.SEARCH_PLACEHOLDER')"
|
:placeholder="searchPlaceholder || t('COMBOBOX.SEARCH_PLACEHOLDER')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<DropdownSection class="max-h-80 overflow-scroll">
|
<DropdownSection class="[&>ul]:max-h-80">
|
||||||
<template v-if="searchResults.length">
|
<template v-if="searchResults.length">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
v-for="option in searchResults"
|
v-for="option in searchResults"
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps, computed } from 'vue';
|
import { defineProps, computed, reactive } from 'vue';
|
||||||
import Message from './Message.vue';
|
import Message from './Message.vue';
|
||||||
import { MESSAGE_TYPES } from './constants.js';
|
import { MESSAGE_TYPES } from './constants.js';
|
||||||
import { useCamelCase } from 'dashboard/composables/useTransformKeys';
|
import { useCamelCase } from 'dashboard/composables/useTransformKeys';
|
||||||
|
import { useMapGetter } from 'dashboard/composables/store.js';
|
||||||
|
import MessageApi from 'dashboard/api/inbox/message.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Props definition for the component
|
* Props definition for the component
|
||||||
@@ -43,6 +45,48 @@ const allMessages = computed(() => {
|
|||||||
return useCamelCase(props.messages, { deep: true });
|
return useCamelCase(props.messages, { deep: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const currentChat = useMapGetter('getSelectedChat');
|
||||||
|
|
||||||
|
// Cache for fetched reply messages to avoid duplicate API calls
|
||||||
|
const fetchedReplyMessages = reactive(new Map());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a specific message from the API by trying to get messages around it
|
||||||
|
* @param {number} messageId - The ID of the message to fetch
|
||||||
|
* @param {number} conversationId - The ID of the conversation
|
||||||
|
* @returns {Promise<Object|null>} - The fetched message or null if not found/error
|
||||||
|
*/
|
||||||
|
const fetchReplyMessage = async (messageId, conversationId) => {
|
||||||
|
// Return cached result if already fetched
|
||||||
|
if (fetchedReplyMessages.has(messageId)) {
|
||||||
|
return fetchedReplyMessages.get(messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await MessageApi.getPreviousMessages({
|
||||||
|
conversationId,
|
||||||
|
before: messageId + 100,
|
||||||
|
after: messageId - 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const messages = response.data?.payload || [];
|
||||||
|
const targetMessage = messages.find(msg => msg.id === messageId);
|
||||||
|
|
||||||
|
if (targetMessage) {
|
||||||
|
const camelCaseMessage = useCamelCase(targetMessage);
|
||||||
|
fetchedReplyMessages.set(messageId, camelCaseMessage);
|
||||||
|
return camelCaseMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache null result to avoid repeated API calls
|
||||||
|
fetchedReplyMessages.set(messageId, null);
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
fetchedReplyMessages.set(messageId, null);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines if a message should be grouped with the next message
|
* Determines if a message should be grouped with the next message
|
||||||
* @param {Number} index - Index of the current message
|
* @param {Number} index - Index of the current message
|
||||||
@@ -90,10 +134,26 @@ const getInReplyToMessage = parentMessage => {
|
|||||||
|
|
||||||
if (!inReplyToMessageId) return null;
|
if (!inReplyToMessageId) return null;
|
||||||
|
|
||||||
// Find in-reply-to message in the messages prop
|
// Try to find in current messages first
|
||||||
const replyMessage = props.messages?.find(
|
let replyMessage = props.messages?.find(msg => msg.id === inReplyToMessageId);
|
||||||
message => message.id === inReplyToMessageId
|
|
||||||
);
|
// Then try store messages
|
||||||
|
if (!replyMessage && currentChat.value?.messages) {
|
||||||
|
replyMessage = currentChat.value.messages.find(
|
||||||
|
msg => msg.id === inReplyToMessageId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then check fetch cache
|
||||||
|
if (!replyMessage && fetchedReplyMessages.has(inReplyToMessageId)) {
|
||||||
|
replyMessage = fetchedReplyMessages.get(inReplyToMessageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If still not found and we have conversation context, fetch it
|
||||||
|
if (!replyMessage && currentChat.value?.id) {
|
||||||
|
fetchReplyMessage(inReplyToMessageId, currentChat.value.id);
|
||||||
|
return null; // Let UI handle loading state
|
||||||
|
}
|
||||||
|
|
||||||
return replyMessage ? useCamelCase(replyMessage) : null;
|
return replyMessage ? useCamelCase(replyMessage) : null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import { sanitizeTextForRender } from '@chatwoot/utils';
|
|||||||
import { allowedCssProperties } from 'lettersanitizer';
|
import { allowedCssProperties } from 'lettersanitizer';
|
||||||
|
|
||||||
import Icon from 'next/icon/Icon.vue';
|
import Icon from 'next/icon/Icon.vue';
|
||||||
import { EmailQuoteExtractor } from './removeReply.js';
|
import { EmailQuoteExtractor } from 'dashboard/helper/emailQuoteExtractor.js';
|
||||||
import BaseBubble from 'next/message/bubbles/Base.vue';
|
|
||||||
import FormattedContent from 'next/message/bubbles/Text/FormattedContent.vue';
|
import FormattedContent from 'next/message/bubbles/Text/FormattedContent.vue';
|
||||||
|
import BaseBubble from 'next/message/bubbles/Base.vue';
|
||||||
import AttachmentChips from 'next/message/chips/AttachmentChips.vue';
|
import AttachmentChips from 'next/message/chips/AttachmentChips.vue';
|
||||||
import EmailMeta from './EmailMeta.vue';
|
import EmailMeta from './EmailMeta.vue';
|
||||||
import TranslationToggle from 'dashboard/components-next/message/TranslationToggle.vue';
|
import TranslationToggle from 'dashboard/components-next/message/TranslationToggle.vue';
|
||||||
@@ -47,6 +47,13 @@ const originalEmailHtml = computed(
|
|||||||
originalEmailText.value
|
originalEmailText.value
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const hasEmailContent = computed(() => {
|
||||||
|
return (
|
||||||
|
contentAttributes?.value?.email?.textContent?.full ||
|
||||||
|
contentAttributes?.value?.email?.htmlContent?.full
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const messageContent = computed(() => {
|
const messageContent = computed(() => {
|
||||||
// If translations exist and we're showing translations (not original)
|
// If translations exist and we're showing translations (not original)
|
||||||
if (hasTranslations.value && !renderOriginal.value) {
|
if (hasTranslations.value && !renderOriginal.value) {
|
||||||
@@ -137,7 +144,7 @@ const handleSeeOriginal = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<FormattedContent
|
<FormattedContent
|
||||||
v-if="isOutgoing && content"
|
v-if="isOutgoing && content && !hasEmailContent"
|
||||||
class="text-n-slate-12"
|
class="text-n-slate-12"
|
||||||
:content="messageContent"
|
:content="messageContent"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -232,6 +232,11 @@ const menuItems = computed(() => {
|
|||||||
label: t('SIDEBAR.CAPTAIN_RESPONSES'),
|
label: t('SIDEBAR.CAPTAIN_RESPONSES'),
|
||||||
to: accountScopedRoute('captain_responses_index'),
|
to: accountScopedRoute('captain_responses_index'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Tools',
|
||||||
|
label: t('SIDEBAR.CAPTAIN_TOOLS'),
|
||||||
|
to: accountScopedRoute('captain_tools_index'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ function changeAvailabilityStatus(availability) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DropdownSection>
|
<DropdownSection class="[&>ul]:overflow-visible">
|
||||||
<div class="grid gap-0">
|
<div class="grid gap-0">
|
||||||
<DropdownItem preserve-open>
|
<DropdownItem preserve-open>
|
||||||
<div class="flex-grow flex items-center gap-1">
|
<div class="flex-grow flex items-center gap-1">
|
||||||
|
|||||||
@@ -118,6 +118,14 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
showQuotedReplyToggle: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
quotedReplyEnabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
emits: [
|
emits: [
|
||||||
'replaceText',
|
'replaceText',
|
||||||
@@ -125,6 +133,7 @@ export default {
|
|||||||
'toggleEditor',
|
'toggleEditor',
|
||||||
'selectWhatsappTemplate',
|
'selectWhatsappTemplate',
|
||||||
'selectContentTemplate',
|
'selectContentTemplate',
|
||||||
|
'toggleQuotedReply',
|
||||||
],
|
],
|
||||||
setup() {
|
setup() {
|
||||||
const { setSignatureFlagForInbox, fetchSignatureFlagFromUISettings } =
|
const { setSignatureFlagForInbox, fetchSignatureFlagFromUISettings } =
|
||||||
@@ -249,6 +258,11 @@ export default {
|
|||||||
isFetchingAppIntegrations() {
|
isFetchingAppIntegrations() {
|
||||||
return this.uiFlags.isFetching;
|
return this.uiFlags.isFetching;
|
||||||
},
|
},
|
||||||
|
quotedReplyToggleTooltip() {
|
||||||
|
return this.quotedReplyEnabled
|
||||||
|
? this.$t('CONVERSATION.REPLYBOX.QUOTED_REPLY.DISABLE_TOOLTIP')
|
||||||
|
: this.$t('CONVERSATION.REPLYBOX.QUOTED_REPLY.ENABLE_TOOLTIP');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
ActiveStorage.start();
|
ActiveStorage.start();
|
||||||
@@ -339,6 +353,16 @@ export default {
|
|||||||
sm
|
sm
|
||||||
@click="toggleMessageSignature"
|
@click="toggleMessageSignature"
|
||||||
/>
|
/>
|
||||||
|
<NextButton
|
||||||
|
v-if="showQuotedReplyToggle"
|
||||||
|
v-tooltip.top-end="quotedReplyToggleTooltip"
|
||||||
|
icon="i-ph-quotes"
|
||||||
|
:variant="quotedReplyEnabled ? 'solid' : 'faded'"
|
||||||
|
color="slate"
|
||||||
|
sm
|
||||||
|
:aria-pressed="quotedReplyEnabled"
|
||||||
|
@click="$emit('toggleQuotedReply')"
|
||||||
|
/>
|
||||||
<NextButton
|
<NextButton
|
||||||
v-if="enableWhatsAppTemplates"
|
v-if="enableWhatsAppTemplates"
|
||||||
v-tooltip.top-end="$t('CONVERSATION.FOOTER.WHATSAPP_TEMPLATES')"
|
v-tooltip.top-end="$t('CONVERSATION.FOOTER.WHATSAPP_TEMPLATES')"
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
quotedEmailText: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
previewText: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['toggle']);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { formatMessage } = useMessageFormatter();
|
||||||
|
|
||||||
|
const isExpanded = ref(false);
|
||||||
|
|
||||||
|
const formattedQuotedEmailText = computed(() => {
|
||||||
|
if (!props.quotedEmailText) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return formatMessage(props.quotedEmailText, false, false, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleExpand = () => {
|
||||||
|
isExpanded.value = !isExpanded.value;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="mt-2">
|
||||||
|
<div
|
||||||
|
class="relative rounded-md px-3 py-2 text-xs text-n-slate-12 bg-n-slate-3 dark:bg-n-solid-3"
|
||||||
|
>
|
||||||
|
<div class="absolute top-2 right-2 z-10 flex items-center gap-1">
|
||||||
|
<NextButton
|
||||||
|
v-tooltip="
|
||||||
|
isExpanded
|
||||||
|
? t('CONVERSATION.REPLYBOX.QUOTED_REPLY.COLLAPSE')
|
||||||
|
: t('CONVERSATION.REPLYBOX.QUOTED_REPLY.EXPAND')
|
||||||
|
"
|
||||||
|
ghost
|
||||||
|
slate
|
||||||
|
xs
|
||||||
|
:icon="isExpanded ? 'i-lucide-minimize' : 'i-lucide-maximize'"
|
||||||
|
@click="toggleExpand"
|
||||||
|
/>
|
||||||
|
<NextButton
|
||||||
|
v-tooltip="t('CONVERSATION.REPLYBOX.QUOTED_REPLY.REMOVE_PREVIEW')"
|
||||||
|
ghost
|
||||||
|
slate
|
||||||
|
xs
|
||||||
|
icon="i-lucide-x"
|
||||||
|
@click="emit('toggle')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-dompurify-html="formattedQuotedEmailText"
|
||||||
|
class="w-full max-w-none break-words prose prose-sm dark:prose-invert cursor-pointer ltr:pr-8 rtl:pl-8"
|
||||||
|
:class="{
|
||||||
|
'line-clamp-1': !isExpanded,
|
||||||
|
'max-h-60 overflow-y-auto': isExpanded,
|
||||||
|
}"
|
||||||
|
:title="previewText"
|
||||||
|
@click="toggleExpand"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -5,6 +5,7 @@ import { useAlert } from 'dashboard/composables';
|
|||||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||||
import { useTrack } from 'dashboard/composables';
|
import { useTrack } from 'dashboard/composables';
|
||||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
|
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||||
|
|
||||||
import CannedResponse from './CannedResponse.vue';
|
import CannedResponse from './CannedResponse.vue';
|
||||||
import ReplyToMessage from './ReplyToMessage.vue';
|
import ReplyToMessage from './ReplyToMessage.vue';
|
||||||
@@ -16,6 +17,7 @@ import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBotto
|
|||||||
import ArticleSearchPopover from 'dashboard/routes/dashboard/helpcenter/components/ArticleSearch/SearchPopover.vue';
|
import ArticleSearchPopover from 'dashboard/routes/dashboard/helpcenter/components/ArticleSearch/SearchPopover.vue';
|
||||||
import MessageSignatureMissingAlert from './MessageSignatureMissingAlert.vue';
|
import MessageSignatureMissingAlert from './MessageSignatureMissingAlert.vue';
|
||||||
import ReplyBoxBanner from './ReplyBoxBanner.vue';
|
import ReplyBoxBanner from './ReplyBoxBanner.vue';
|
||||||
|
import QuotedEmailPreview from './QuotedEmailPreview.vue';
|
||||||
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
|
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
|
||||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||||
import AudioRecorder from 'dashboard/components/widgets/WootWriter/AudioRecorder.vue';
|
import AudioRecorder from 'dashboard/components/widgets/WootWriter/AudioRecorder.vue';
|
||||||
@@ -32,6 +34,12 @@ import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
|
|||||||
import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
|
import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
|
||||||
import { trimContent, debounce, getRecipients } from '@chatwoot/utils';
|
import { trimContent, debounce, getRecipients } from '@chatwoot/utils';
|
||||||
import wootConstants from 'dashboard/constants/globals';
|
import wootConstants from 'dashboard/constants/globals';
|
||||||
|
import {
|
||||||
|
extractQuotedEmailText,
|
||||||
|
buildQuotedEmailHeader,
|
||||||
|
truncatePreviewText,
|
||||||
|
appendQuotedTextToMessage,
|
||||||
|
} from 'dashboard/helper/quotedEmailHelper';
|
||||||
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||||
import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
|
import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
|
||||||
import {
|
import {
|
||||||
@@ -65,6 +73,7 @@ export default {
|
|||||||
ContentTemplates,
|
ContentTemplates,
|
||||||
WhatsappTemplates,
|
WhatsappTemplates,
|
||||||
WootMessageEditor,
|
WootMessageEditor,
|
||||||
|
QuotedEmailPreview,
|
||||||
},
|
},
|
||||||
mixins: [inboxMixin, fileUploadMixin, keyboardEventListenerMixins],
|
mixins: [inboxMixin, fileUploadMixin, keyboardEventListenerMixins],
|
||||||
props: {
|
props: {
|
||||||
@@ -80,6 +89,8 @@ export default {
|
|||||||
updateUISettings,
|
updateUISettings,
|
||||||
isEditorHotKeyEnabled,
|
isEditorHotKeyEnabled,
|
||||||
fetchSignatureFlagFromUISettings,
|
fetchSignatureFlagFromUISettings,
|
||||||
|
setQuotedReplyFlagForInbox,
|
||||||
|
fetchQuotedReplyFlagFromUISettings,
|
||||||
} = useUISettings();
|
} = useUISettings();
|
||||||
|
|
||||||
const replyEditor = useTemplateRef('replyEditor');
|
const replyEditor = useTemplateRef('replyEditor');
|
||||||
@@ -89,6 +100,8 @@ export default {
|
|||||||
updateUISettings,
|
updateUISettings,
|
||||||
isEditorHotKeyEnabled,
|
isEditorHotKeyEnabled,
|
||||||
fetchSignatureFlagFromUISettings,
|
fetchSignatureFlagFromUISettings,
|
||||||
|
setQuotedReplyFlagForInbox,
|
||||||
|
fetchQuotedReplyFlagFromUISettings,
|
||||||
replyEditor,
|
replyEditor,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -130,6 +143,8 @@ export default {
|
|||||||
currentUser: 'getCurrentUser',
|
currentUser: 'getCurrentUser',
|
||||||
lastEmail: 'getLastEmailInSelectedChat',
|
lastEmail: 'getLastEmailInSelectedChat',
|
||||||
globalConfig: 'globalConfig/get',
|
globalConfig: 'globalConfig/get',
|
||||||
|
accountId: 'getCurrentAccountId',
|
||||||
|
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||||
}),
|
}),
|
||||||
currentContact() {
|
currentContact() {
|
||||||
return this.$store.getters['contacts/getContact'](
|
return this.$store.getters['contacts/getContact'](
|
||||||
@@ -367,6 +382,51 @@ export default {
|
|||||||
const { slug = '' } = portal;
|
const { slug = '' } = portal;
|
||||||
return slug;
|
return slug;
|
||||||
},
|
},
|
||||||
|
isQuotedEmailReplyEnabled() {
|
||||||
|
return this.isFeatureEnabledonAccount(
|
||||||
|
this.accountId,
|
||||||
|
FEATURE_FLAGS.QUOTED_EMAIL_REPLY
|
||||||
|
);
|
||||||
|
},
|
||||||
|
quotedReplyPreference() {
|
||||||
|
if (!this.isAnEmailChannel || !this.isQuotedEmailReplyEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !!this.fetchQuotedReplyFlagFromUISettings(this.channelType);
|
||||||
|
},
|
||||||
|
lastEmailWithQuotedContent() {
|
||||||
|
if (!this.isAnEmailChannel) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastEmail = this.lastEmail;
|
||||||
|
if (!lastEmail || lastEmail.private) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastEmail;
|
||||||
|
},
|
||||||
|
quotedEmailText() {
|
||||||
|
return extractQuotedEmailText(this.lastEmailWithQuotedContent);
|
||||||
|
},
|
||||||
|
quotedEmailPreviewText() {
|
||||||
|
return truncatePreviewText(this.quotedEmailText, 80);
|
||||||
|
},
|
||||||
|
shouldShowQuotedReplyToggle() {
|
||||||
|
return (
|
||||||
|
this.isAnEmailChannel &&
|
||||||
|
!this.isOnPrivateNote &&
|
||||||
|
this.isQuotedEmailReplyEnabled
|
||||||
|
);
|
||||||
|
},
|
||||||
|
shouldShowQuotedPreview() {
|
||||||
|
return (
|
||||||
|
this.shouldShowQuotedReplyToggle &&
|
||||||
|
this.quotedReplyPreference &&
|
||||||
|
!!this.quotedEmailText
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
currentChat(conversation, oldConversation) {
|
currentChat(conversation, oldConversation) {
|
||||||
@@ -516,6 +576,36 @@ export default {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
toggleQuotedReply() {
|
||||||
|
if (!this.isAnEmailChannel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextValue = !this.quotedReplyPreference;
|
||||||
|
this.setQuotedReplyFlagForInbox(this.channelType, nextValue);
|
||||||
|
},
|
||||||
|
shouldIncludeQuotedEmail() {
|
||||||
|
return (
|
||||||
|
this.isQuotedEmailReplyEnabled &&
|
||||||
|
this.quotedReplyPreference &&
|
||||||
|
this.shouldShowQuotedReplyToggle &&
|
||||||
|
!!this.quotedEmailText
|
||||||
|
);
|
||||||
|
},
|
||||||
|
getMessageWithQuotedEmailText(message) {
|
||||||
|
if (!this.shouldIncludeQuotedEmail()) {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
const quotedText = this.quotedEmailText || '';
|
||||||
|
const header = buildQuotedEmailHeader(
|
||||||
|
this.lastEmailWithQuotedContent,
|
||||||
|
this.currentContact,
|
||||||
|
this.inbox
|
||||||
|
);
|
||||||
|
|
||||||
|
return appendQuotedTextToMessage(message, quotedText, header);
|
||||||
|
},
|
||||||
resetRecorderAndClearAttachments() {
|
resetRecorderAndClearAttachments() {
|
||||||
// Reset audio recorder UI state
|
// Reset audio recorder UI state
|
||||||
this.resetAudioRecorderInput();
|
this.resetAudioRecorderInput();
|
||||||
@@ -965,9 +1055,11 @@ export default {
|
|||||||
return multipleMessagePayload;
|
return multipleMessagePayload;
|
||||||
},
|
},
|
||||||
getMessagePayload(message) {
|
getMessagePayload(message) {
|
||||||
|
const messageWithQuote = this.getMessageWithQuotedEmailText(message);
|
||||||
|
|
||||||
let messagePayload = {
|
let messagePayload = {
|
||||||
conversationId: this.currentChat.id,
|
conversationId: this.currentChat.id,
|
||||||
message,
|
message: messageWithQuote,
|
||||||
private: this.isPrivate,
|
private: this.isPrivate,
|
||||||
sender: this.sender,
|
sender: this.sender,
|
||||||
};
|
};
|
||||||
@@ -995,7 +1087,6 @@ export default {
|
|||||||
if (this.toEmails && !this.isOnPrivateNote) {
|
if (this.toEmails && !this.isOnPrivateNote) {
|
||||||
messagePayload.toEmails = this.toEmails;
|
messagePayload.toEmails = this.toEmails;
|
||||||
}
|
}
|
||||||
|
|
||||||
return messagePayload;
|
return messagePayload;
|
||||||
},
|
},
|
||||||
setCcEmails(value) {
|
setCcEmails(value) {
|
||||||
@@ -1160,6 +1251,12 @@ export default {
|
|||||||
@toggle-variables-menu="toggleVariablesMenu"
|
@toggle-variables-menu="toggleVariablesMenu"
|
||||||
@clear-selection="clearEditorSelection"
|
@clear-selection="clearEditorSelection"
|
||||||
/>
|
/>
|
||||||
|
<QuotedEmailPreview
|
||||||
|
v-if="shouldShowQuotedPreview"
|
||||||
|
:quoted-email-text="quotedEmailText"
|
||||||
|
:preview-text="quotedEmailPreviewText"
|
||||||
|
@toggle="toggleQuotedReply"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="hasAttachments && !showAudioRecorderEditor"
|
v-if="hasAttachments && !showAudioRecorderEditor"
|
||||||
@@ -1195,6 +1292,8 @@ export default {
|
|||||||
:show-editor-toggle="isAPIInbox && !isOnPrivateNote"
|
:show-editor-toggle="isAPIInbox && !isOnPrivateNote"
|
||||||
:show-emoji-picker="showEmojiPicker"
|
:show-emoji-picker="showEmojiPicker"
|
||||||
:show-file-upload="showFileUpload"
|
:show-file-upload="showFileUpload"
|
||||||
|
:show-quoted-reply-toggle="shouldShowQuotedReplyToggle"
|
||||||
|
:quoted-reply-enabled="quotedReplyPreference"
|
||||||
:toggle-audio-recorder-play-pause="toggleAudioRecorderPlayPause"
|
:toggle-audio-recorder-play-pause="toggleAudioRecorderPlayPause"
|
||||||
:toggle-audio-recorder="toggleAudioRecorder"
|
:toggle-audio-recorder="toggleAudioRecorder"
|
||||||
:toggle-emoji-picker="toggleEmojiPicker"
|
:toggle-emoji-picker="toggleEmojiPicker"
|
||||||
@@ -1206,6 +1305,7 @@ export default {
|
|||||||
@toggle-editor="toggleRichContentEditor"
|
@toggle-editor="toggleRichContentEditor"
|
||||||
@replace-text="replaceText"
|
@replace-text="replaceText"
|
||||||
@toggle-insert-article="toggleInsertArticle"
|
@toggle-insert-article="toggleInsertArticle"
|
||||||
|
@toggle-quoted-reply="toggleQuotedReply"
|
||||||
/>
|
/>
|
||||||
<WhatsappTemplates
|
<WhatsappTemplates
|
||||||
:inbox-id="inbox.id"
|
:inbox-id="inbox.id"
|
||||||
|
|||||||
@@ -32,7 +32,10 @@ export default {
|
|||||||
value: {
|
value: {
|
||||||
required,
|
required,
|
||||||
isEqual(value) {
|
isEqual(value) {
|
||||||
return value === this.confirmValue;
|
// Trim whitespace from both input and target values
|
||||||
|
const normalizedInput = (value || '').trim();
|
||||||
|
const normalizedTarget = (this.confirmValue || '').trim();
|
||||||
|
return normalizedInput === normalizedTarget;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const getUISettingsMock = ref({
|
|||||||
conversation_sidebar_items_order: DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
conversation_sidebar_items_order: DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
||||||
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
|
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
|
||||||
editor_message_key: 'enter',
|
editor_message_key: 'enter',
|
||||||
|
channel_email_quoted_reply_enabled: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock('dashboard/composables/store', () => ({
|
vi.mock('dashboard/composables/store', () => ({
|
||||||
@@ -37,6 +38,7 @@ describe('useUISettings', () => {
|
|||||||
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
||||||
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
|
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
|
||||||
editor_message_key: 'enter',
|
editor_message_key: 'enter',
|
||||||
|
channel_email_quoted_reply_enabled: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -51,6 +53,7 @@ describe('useUISettings', () => {
|
|||||||
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
||||||
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
|
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
|
||||||
editor_message_key: 'enter',
|
editor_message_key: 'enter',
|
||||||
|
channel_email_quoted_reply_enabled: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -65,6 +68,7 @@ describe('useUISettings', () => {
|
|||||||
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
||||||
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
|
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
|
||||||
editor_message_key: 'enter',
|
editor_message_key: 'enter',
|
||||||
|
channel_email_quoted_reply_enabled: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -100,6 +104,7 @@ describe('useUISettings', () => {
|
|||||||
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
|
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
|
||||||
email_signature_enabled: true,
|
email_signature_enabled: true,
|
||||||
editor_message_key: 'enter',
|
editor_message_key: 'enter',
|
||||||
|
channel_email_quoted_reply_enabled: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -109,6 +114,26 @@ describe('useUISettings', () => {
|
|||||||
expect(fetchSignatureFlagFromUISettings('email')).toBe(undefined);
|
expect(fetchSignatureFlagFromUISettings('email')).toBe(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('sets quoted reply flag for inbox correctly', () => {
|
||||||
|
const { setQuotedReplyFlagForInbox } = useUISettings();
|
||||||
|
setQuotedReplyFlagForInbox('Channel::Email', false);
|
||||||
|
expect(mockDispatch).toHaveBeenCalledWith('updateUISettings', {
|
||||||
|
uiSettings: {
|
||||||
|
is_ct_labels_open: true,
|
||||||
|
conversation_sidebar_items_order:
|
||||||
|
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
||||||
|
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
|
||||||
|
editor_message_key: 'enter',
|
||||||
|
channel_email_quoted_reply_enabled: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fetches quoted reply flag from UI settings correctly', () => {
|
||||||
|
const { fetchQuotedReplyFlagFromUISettings } = useUISettings();
|
||||||
|
expect(fetchQuotedReplyFlagFromUISettings('Channel::Email')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('returns correct value for isEditorHotKeyEnabled when editor_message_key is configured', () => {
|
it('returns correct value for isEditorHotKeyEnabled when editor_message_key is configured', () => {
|
||||||
getUISettingsMock.value.enter_to_send_enabled = false;
|
getUISettingsMock.value.enter_to_send_enabled = false;
|
||||||
const { isEditorHotKeyEnabled } = useUISettings();
|
const { isEditorHotKeyEnabled } = useUISettings();
|
||||||
|
|||||||
@@ -87,6 +87,13 @@ const setSignatureFlagForInbox = (channelType, value, updateUISettings) => {
|
|||||||
updateUISettings({ [`${slugifiedChannel}_signature_enabled`]: value });
|
updateUISettings({ [`${slugifiedChannel}_signature_enabled`]: value });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setQuotedReplyFlagForInbox = (channelType, value, updateUISettings) => {
|
||||||
|
if (!channelType) return;
|
||||||
|
|
||||||
|
const slugifiedChannel = slugifyChannel(channelType);
|
||||||
|
updateUISettings({ [`${slugifiedChannel}_quoted_reply_enabled`]: value });
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the signature flag for a specific channel type from UI settings.
|
* Fetches the signature flag for a specific channel type from UI settings.
|
||||||
* @param {string} channelType - The type of the channel.
|
* @param {string} channelType - The type of the channel.
|
||||||
@@ -100,6 +107,13 @@ const fetchSignatureFlagFromUISettings = (channelType, uiSettings) => {
|
|||||||
return uiSettings.value[`${slugifiedChannel}_signature_enabled`];
|
return uiSettings.value[`${slugifiedChannel}_signature_enabled`];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchQuotedReplyFlagFromUISettings = (channelType, uiSettings) => {
|
||||||
|
if (!channelType) return false;
|
||||||
|
|
||||||
|
const slugifiedChannel = slugifyChannel(channelType);
|
||||||
|
return uiSettings.value[`${slugifiedChannel}_quoted_reply_enabled`];
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a specific editor hotkey is enabled.
|
* Checks if a specific editor hotkey is enabled.
|
||||||
* @param {string} key - The key to check.
|
* @param {string} key - The key to check.
|
||||||
@@ -147,6 +161,10 @@ export function useUISettings() {
|
|||||||
setSignatureFlagForInbox(channelType, value, updateUISettings),
|
setSignatureFlagForInbox(channelType, value, updateUISettings),
|
||||||
fetchSignatureFlagFromUISettings: channelType =>
|
fetchSignatureFlagFromUISettings: channelType =>
|
||||||
fetchSignatureFlagFromUISettings(channelType, uiSettings),
|
fetchSignatureFlagFromUISettings(channelType, uiSettings),
|
||||||
|
setQuotedReplyFlagForInbox: (channelType, value) =>
|
||||||
|
setQuotedReplyFlagForInbox(channelType, value, updateUISettings),
|
||||||
|
fetchQuotedReplyFlagFromUISettings: channelType =>
|
||||||
|
fetchQuotedReplyFlagFromUISettings(channelType, uiSettings),
|
||||||
isEditorHotKeyEnabled: key => isEditorHotKeyEnabled(key, uiSettings),
|
isEditorHotKeyEnabled: key => isEditorHotKeyEnabled(key, uiSettings),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export const FEATURE_FLAGS = {
|
|||||||
CONTACT_CHATWOOT_SUPPORT_TEAM: 'contact_chatwoot_support_team',
|
CONTACT_CHATWOOT_SUPPORT_TEAM: 'contact_chatwoot_support_team',
|
||||||
CAPTAIN_V2: 'captain_integration_v2',
|
CAPTAIN_V2: 'captain_integration_v2',
|
||||||
SAML: 'saml',
|
SAML: 'saml',
|
||||||
|
QUOTED_EMAIL_REPLY: 'quoted_email_reply',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PREMIUM_FEATURES = [
|
export const PREMIUM_FEATURES = [
|
||||||
@@ -48,5 +49,5 @@ export const PREMIUM_FEATURES = [
|
|||||||
FEATURE_FLAGS.CUSTOM_ROLES,
|
FEATURE_FLAGS.CUSTOM_ROLES,
|
||||||
FEATURE_FLAGS.AUDIT_LOGS,
|
FEATURE_FLAGS.AUDIT_LOGS,
|
||||||
FEATURE_FLAGS.HELP_CENTER,
|
FEATURE_FLAGS.HELP_CENTER,
|
||||||
FEATURE_FLAGS.CAPTAIN_V2,
|
FEATURE_FLAGS.SAML,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -145,3 +145,34 @@ export const extractFilenameFromUrl = url => {
|
|||||||
return match ? match[1] : url;
|
return match ? match[1] : url;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes a comma/newline separated list of domains
|
||||||
|
* @param {string} domains - The comma/newline separated list of domains
|
||||||
|
* @returns {string} - The normalized list of domains
|
||||||
|
* - Converts newlines to commas
|
||||||
|
* - Trims whitespace
|
||||||
|
* - Lowercases entries
|
||||||
|
* - Removes empty values
|
||||||
|
* - De-duplicates while preserving original order
|
||||||
|
*/
|
||||||
|
export const sanitizeAllowedDomains = domains => {
|
||||||
|
if (!domains) return '';
|
||||||
|
|
||||||
|
const tokens = domains
|
||||||
|
.replace(/\r\n/g, '\n')
|
||||||
|
.replace(/\s*\n\s*/g, ',')
|
||||||
|
.split(',')
|
||||||
|
.map(d => d.trim().toLowerCase())
|
||||||
|
.filter(d => d.length > 0);
|
||||||
|
|
||||||
|
// De-duplicate while preserving order using Set and filter index
|
||||||
|
const seen = new Set();
|
||||||
|
const unique = tokens.filter(d => {
|
||||||
|
if (seen.has(d)) return false;
|
||||||
|
seen.add(d);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return unique.join(',');
|
||||||
|
};
|
||||||
|
|||||||
+30
@@ -10,6 +10,8 @@ const QUOTE_INDICATORS = [
|
|||||||
'[class*="Quote"]',
|
'[class*="Quote"]',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const BLOCKQUOTE_FALLBACK_SELECTOR = 'blockquote';
|
||||||
|
|
||||||
// Regex patterns for quote identification
|
// Regex patterns for quote identification
|
||||||
const QUOTE_PATTERNS = [
|
const QUOTE_PATTERNS = [
|
||||||
/On .* wrote:/i,
|
/On .* wrote:/i,
|
||||||
@@ -36,6 +38,8 @@ export class EmailQuoteExtractor {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.removeTrailingBlockquote(tempDiv);
|
||||||
|
|
||||||
// Remove text-based quotes
|
// Remove text-based quotes
|
||||||
const textNodeQuotes = this.findTextNodeQuotes(tempDiv);
|
const textNodeQuotes = this.findTextNodeQuotes(tempDiv);
|
||||||
textNodeQuotes.forEach(el => {
|
textNodeQuotes.forEach(el => {
|
||||||
@@ -62,6 +66,10 @@ export class EmailQuoteExtractor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.findTrailingBlockquote(tempDiv)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Check for text-based quotes
|
// Check for text-based quotes
|
||||||
const textNodeQuotes = this.findTextNodeQuotes(tempDiv);
|
const textNodeQuotes = this.findTextNodeQuotes(tempDiv);
|
||||||
return textNodeQuotes.length > 0;
|
return textNodeQuotes.length > 0;
|
||||||
@@ -123,4 +131,26 @@ export class EmailQuoteExtractor {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove fallback blockquote if it is the last top-level element.
|
||||||
|
* @param {Element} rootElement - Root element containing the HTML
|
||||||
|
*/
|
||||||
|
static removeTrailingBlockquote(rootElement) {
|
||||||
|
const trailingBlockquote = this.findTrailingBlockquote(rootElement);
|
||||||
|
trailingBlockquote?.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Locate a fallback blockquote that is the last top-level element.
|
||||||
|
* @param {Element} rootElement - Root element containing the HTML
|
||||||
|
* @returns {Element|null} The trailing blockquote element if present
|
||||||
|
*/
|
||||||
|
static findTrailingBlockquote(rootElement) {
|
||||||
|
const lastElement = rootElement.lastElementChild;
|
||||||
|
if (lastElement?.matches?.(BLOCKQUOTE_FALLBACK_SELECTOR)) {
|
||||||
|
return lastElement;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -68,13 +68,17 @@ export const registerSubscription = (onSuccess = () => {}) => {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
onSuccess();
|
onSuccess();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(error => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error('Push subscription registration failed:', error);
|
||||||
useAlert('This browser does not support desktop notification');
|
useAlert('This browser does not support desktop notification');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const requestPushPermissions = ({ onSuccess }) => {
|
export const requestPushPermissions = ({ onSuccess }) => {
|
||||||
if (!('Notification' in window)) {
|
if (!('Notification' in window)) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn('Notification is not supported');
|
||||||
useAlert('This browser does not support desktop notification');
|
useAlert('This browser does not support desktop notification');
|
||||||
} else if (Notification.permission === 'granted') {
|
} else if (Notification.permission === 'granted') {
|
||||||
registerSubscription(onSuccess);
|
registerSubscription(onSuccess);
|
||||||
|
|||||||
@@ -0,0 +1,332 @@
|
|||||||
|
import { format, parseISO, isValid as isValidDate } from 'date-fns';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts plain text from HTML content
|
||||||
|
* @param {string} html - HTML content to convert
|
||||||
|
* @returns {string} Plain text content
|
||||||
|
*/
|
||||||
|
export const extractPlainTextFromHtml = html => {
|
||||||
|
if (!html) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (typeof document === 'undefined') {
|
||||||
|
return html.replace(/<[^>]*>/g, ' ');
|
||||||
|
}
|
||||||
|
const tempDiv = document.createElement('div');
|
||||||
|
tempDiv.innerHTML = html;
|
||||||
|
return tempDiv.textContent || tempDiv.innerText || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts sender name from email message
|
||||||
|
* @param {Object} lastEmail - Last email message object
|
||||||
|
* @param {Object} contact - Contact object
|
||||||
|
* @returns {string} Sender name
|
||||||
|
*/
|
||||||
|
export const getEmailSenderName = (lastEmail, contact) => {
|
||||||
|
const senderName = lastEmail?.sender?.name;
|
||||||
|
if (senderName && senderName.trim()) {
|
||||||
|
return senderName.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const contactName = contact?.name;
|
||||||
|
return contactName && contactName.trim() ? contactName.trim() : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts sender email from email message
|
||||||
|
* @param {Object} lastEmail - Last email message object
|
||||||
|
* @param {Object} contact - Contact object
|
||||||
|
* @returns {string} Sender email address
|
||||||
|
*/
|
||||||
|
export const getEmailSenderEmail = (lastEmail, contact) => {
|
||||||
|
const senderEmail = lastEmail?.sender?.email;
|
||||||
|
if (senderEmail && senderEmail.trim()) {
|
||||||
|
return senderEmail.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentAttributes =
|
||||||
|
lastEmail?.contentAttributes || lastEmail?.content_attributes || {};
|
||||||
|
const emailMeta = contentAttributes.email || {};
|
||||||
|
|
||||||
|
if (Array.isArray(emailMeta.from) && emailMeta.from.length > 0) {
|
||||||
|
const fromAddress = emailMeta.from[0];
|
||||||
|
if (fromAddress && fromAddress.trim()) {
|
||||||
|
return fromAddress.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const contactEmail = contact?.email;
|
||||||
|
return contactEmail && contactEmail.trim() ? contactEmail.trim() : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts date from email message
|
||||||
|
* @param {Object} lastEmail - Last email message object
|
||||||
|
* @returns {Date|null} Email date
|
||||||
|
*/
|
||||||
|
export const getEmailDate = lastEmail => {
|
||||||
|
const contentAttributes =
|
||||||
|
lastEmail?.contentAttributes || lastEmail?.content_attributes || {};
|
||||||
|
const emailMeta = contentAttributes.email || {};
|
||||||
|
|
||||||
|
if (emailMeta.date) {
|
||||||
|
const parsedDate = parseISO(emailMeta.date);
|
||||||
|
if (isValidDate(parsedDate)) {
|
||||||
|
return parsedDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdAt = lastEmail?.created_at;
|
||||||
|
if (createdAt) {
|
||||||
|
const timestamp = Number(createdAt);
|
||||||
|
if (!Number.isNaN(timestamp)) {
|
||||||
|
const milliseconds = timestamp > 1e12 ? timestamp : timestamp * 1000;
|
||||||
|
const derivedDate = new Date(milliseconds);
|
||||||
|
if (!Number.isNaN(derivedDate.getTime())) {
|
||||||
|
return derivedDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats date for quoted email header
|
||||||
|
* @param {Date} date - Date to format
|
||||||
|
* @returns {string} Formatted date string
|
||||||
|
*/
|
||||||
|
export const formatQuotedEmailDate = date => {
|
||||||
|
try {
|
||||||
|
return format(date, "EEE, MMM d, yyyy 'at' p");
|
||||||
|
} catch (error) {
|
||||||
|
const fallbackDate = new Date(date);
|
||||||
|
if (!Number.isNaN(fallbackDate.getTime())) {
|
||||||
|
return format(fallbackDate, "EEE, MMM d, yyyy 'at' p");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts inbox email address from last email message
|
||||||
|
* @param {Object} lastEmail - Last email message object
|
||||||
|
* @param {Object} inbox - Inbox object
|
||||||
|
* @returns {string} Inbox email address
|
||||||
|
*/
|
||||||
|
export const getInboxEmail = (lastEmail, inbox) => {
|
||||||
|
const contentAttributes =
|
||||||
|
lastEmail?.contentAttributes || lastEmail?.content_attributes || {};
|
||||||
|
const emailMeta = contentAttributes.email || {};
|
||||||
|
|
||||||
|
if (Array.isArray(emailMeta.to) && emailMeta.to.length > 0) {
|
||||||
|
const toAddress = emailMeta.to[0];
|
||||||
|
if (toAddress && toAddress.trim()) {
|
||||||
|
return toAddress.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const inboxEmail = inbox?.email;
|
||||||
|
return inboxEmail && inboxEmail.trim() ? inboxEmail.trim() : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds quoted email header from contact (for incoming messages)
|
||||||
|
* @param {Object} lastEmail - Last email message object
|
||||||
|
* @param {Object} contact - Contact object
|
||||||
|
* @returns {string} Formatted header string
|
||||||
|
*/
|
||||||
|
export const buildQuotedEmailHeaderFromContact = (lastEmail, contact) => {
|
||||||
|
if (!lastEmail) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const quotedDate = getEmailDate(lastEmail);
|
||||||
|
const senderEmail = getEmailSenderEmail(lastEmail, contact);
|
||||||
|
|
||||||
|
if (!quotedDate || !senderEmail) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedDate = formatQuotedEmailDate(quotedDate);
|
||||||
|
if (!formattedDate) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const senderName = getEmailSenderName(lastEmail, contact);
|
||||||
|
const hasName = !!senderName;
|
||||||
|
const contactLabel = hasName
|
||||||
|
? `${senderName} <${senderEmail}>`
|
||||||
|
: `<${senderEmail}>`;
|
||||||
|
|
||||||
|
return `On ${formattedDate} ${contactLabel} wrote:`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds quoted email header from inbox (for outgoing messages)
|
||||||
|
* @param {Object} lastEmail - Last email message object
|
||||||
|
* @param {Object} inbox - Inbox object
|
||||||
|
* @returns {string} Formatted header string
|
||||||
|
*/
|
||||||
|
export const buildQuotedEmailHeaderFromInbox = (lastEmail, inbox) => {
|
||||||
|
if (!lastEmail) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const quotedDate = getEmailDate(lastEmail);
|
||||||
|
const inboxEmail = getInboxEmail(lastEmail, inbox);
|
||||||
|
|
||||||
|
if (!quotedDate || !inboxEmail) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedDate = formatQuotedEmailDate(quotedDate);
|
||||||
|
if (!formattedDate) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const inboxName = inbox?.name;
|
||||||
|
const hasName = !!inboxName;
|
||||||
|
const inboxLabel = hasName
|
||||||
|
? `${inboxName} <${inboxEmail}>`
|
||||||
|
: `<${inboxEmail}>`;
|
||||||
|
|
||||||
|
return `On ${formattedDate} ${inboxLabel} wrote:`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds quoted email header based on message type
|
||||||
|
* @param {Object} lastEmail - Last email message object
|
||||||
|
* @param {Object} contact - Contact object
|
||||||
|
* @param {Object} inbox - Inbox object
|
||||||
|
* @returns {string} Formatted header string
|
||||||
|
*/
|
||||||
|
export const buildQuotedEmailHeader = (lastEmail, contact, inbox) => {
|
||||||
|
if (!lastEmail) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// MESSAGE_TYPE.OUTGOING = 1, MESSAGE_TYPE.INCOMING = 0
|
||||||
|
const isOutgoing = lastEmail.message_type === 1;
|
||||||
|
|
||||||
|
if (isOutgoing) {
|
||||||
|
return buildQuotedEmailHeaderFromInbox(lastEmail, inbox);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildQuotedEmailHeaderFromContact(lastEmail, contact);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats text as markdown blockquote
|
||||||
|
* @param {string} text - Text to format
|
||||||
|
* @param {string} header - Optional header to prepend
|
||||||
|
* @returns {string} Formatted blockquote
|
||||||
|
*/
|
||||||
|
export const formatQuotedTextAsBlockquote = (text, header = '') => {
|
||||||
|
const normalizedLines = text
|
||||||
|
? String(text).replace(/\r\n/g, '\n').split('\n')
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (!header && !normalizedLines.length) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const quotedLines = [];
|
||||||
|
|
||||||
|
if (header) {
|
||||||
|
quotedLines.push(`> ${header}`);
|
||||||
|
quotedLines.push('>');
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizedLines.forEach(line => {
|
||||||
|
const trimmedLine = line.trimEnd();
|
||||||
|
quotedLines.push(trimmedLine ? `> ${trimmedLine}` : '>');
|
||||||
|
});
|
||||||
|
|
||||||
|
return quotedLines.join('\n');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts quoted email text from last email message
|
||||||
|
* @param {Object} lastEmail - Last email message object
|
||||||
|
* @returns {string} Quoted email text
|
||||||
|
*/
|
||||||
|
export const extractQuotedEmailText = lastEmail => {
|
||||||
|
if (!lastEmail) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentAttributes =
|
||||||
|
lastEmail.contentAttributes || lastEmail.content_attributes || {};
|
||||||
|
const emailContent = contentAttributes.email || {};
|
||||||
|
const textContent = emailContent.textContent || emailContent.text_content;
|
||||||
|
|
||||||
|
if (textContent?.reply) {
|
||||||
|
return textContent.reply;
|
||||||
|
}
|
||||||
|
if (textContent?.full) {
|
||||||
|
return textContent.full;
|
||||||
|
}
|
||||||
|
|
||||||
|
const htmlContent = emailContent.htmlContent || emailContent.html_content;
|
||||||
|
if (htmlContent?.reply) {
|
||||||
|
return extractPlainTextFromHtml(htmlContent.reply);
|
||||||
|
}
|
||||||
|
if (htmlContent?.full) {
|
||||||
|
return extractPlainTextFromHtml(htmlContent.full);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallbackContent =
|
||||||
|
lastEmail.content || lastEmail.processed_message_content || '';
|
||||||
|
|
||||||
|
return fallbackContent;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Truncates text for preview display
|
||||||
|
* @param {string} text - Text to truncate
|
||||||
|
* @param {number} maxLength - Maximum length (default: 80)
|
||||||
|
* @returns {string} Truncated text
|
||||||
|
*/
|
||||||
|
export const truncatePreviewText = (text, maxLength = 80) => {
|
||||||
|
const preview = text.trim().replace(/\s+/g, ' ');
|
||||||
|
if (!preview) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preview.length <= maxLength) {
|
||||||
|
return preview;
|
||||||
|
}
|
||||||
|
return `${preview.slice(0, maxLength - 3)}...`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends quoted text to message
|
||||||
|
* @param {string} message - Original message
|
||||||
|
* @param {string} quotedText - Text to quote
|
||||||
|
* @param {string} header - Quote header
|
||||||
|
* @returns {string} Message with quoted text appended
|
||||||
|
*/
|
||||||
|
export const appendQuotedTextToMessage = (message, quotedText, header) => {
|
||||||
|
const baseMessage = message ? String(message) : '';
|
||||||
|
const quotedBlock = formatQuotedTextAsBlockquote(quotedText, header);
|
||||||
|
|
||||||
|
if (!quotedBlock) {
|
||||||
|
return baseMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!baseMessage) {
|
||||||
|
return quotedBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
let separator = '\n\n';
|
||||||
|
if (baseMessage.endsWith('\n\n')) {
|
||||||
|
separator = '';
|
||||||
|
} else if (baseMessage.endsWith('\n')) {
|
||||||
|
separator = '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${baseMessage}${separator}${quotedBlock}`;
|
||||||
|
};
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
timeStampAppendedURL,
|
timeStampAppendedURL,
|
||||||
getHostNameFromURL,
|
getHostNameFromURL,
|
||||||
extractFilenameFromUrl,
|
extractFilenameFromUrl,
|
||||||
|
sanitizeAllowedDomains,
|
||||||
} from '../URLHelper';
|
} from '../URLHelper';
|
||||||
|
|
||||||
describe('#URL Helpers', () => {
|
describe('#URL Helpers', () => {
|
||||||
@@ -318,4 +319,32 @@ describe('#URL Helpers', () => {
|
|||||||
).toBe('file.doc');
|
).toBe('file.doc');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('sanitizeAllowedDomains', () => {
|
||||||
|
it('returns empty string for falsy input', () => {
|
||||||
|
expect(sanitizeAllowedDomains('')).toBe('');
|
||||||
|
expect(sanitizeAllowedDomains(null)).toBe('');
|
||||||
|
expect(sanitizeAllowedDomains(undefined)).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('trims whitespace and converts newlines to commas', () => {
|
||||||
|
const input = ' example.com \n foo.bar\nbar.baz ';
|
||||||
|
expect(sanitizeAllowedDomains(input)).toBe('example.com,foo.bar,bar.baz');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles Windows newlines and mixed spacing', () => {
|
||||||
|
const input = ' example.com\r\n\tfoo.bar , bar.baz ';
|
||||||
|
expect(sanitizeAllowedDomains(input)).toBe('example.com,foo.bar,bar.baz');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes empty values from repeated commas', () => {
|
||||||
|
const input = ',,example.com,,foo.bar,,';
|
||||||
|
expect(sanitizeAllowedDomains(input)).toBe('example.com,foo.bar');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lowercases entries and de-duplicates preserving order', () => {
|
||||||
|
const input = 'Example.com,FOO.bar,example.com,Bar.Baz,foo.BAR';
|
||||||
|
expect(sanitizeAllowedDomains(input)).toBe('example.com,foo.bar,bar.baz');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { EmailQuoteExtractor } from '../emailQuoteExtractor.js';
|
||||||
|
|
||||||
|
const SAMPLE_EMAIL_HTML = `
|
||||||
|
<p>method</p>
|
||||||
|
<blockquote>
|
||||||
|
<p>On Mon, Sep 29, 2025 at 5:18 PM John <a href="mailto:shivam@chatwoot.com">shivam@chatwoot.com</a> wrote:</p>
|
||||||
|
<p>Hi</p>
|
||||||
|
<blockquote>
|
||||||
|
<p>On Mon, Sep 29, 2025 at 5:17 PM Shivam Mishra <a href="mailto:shivam@chatwoot.com">shivam@chatwoot.com</a> wrote:</p>
|
||||||
|
<p>Yes, it is.</p>
|
||||||
|
<p>On Mon, Sep 29, 2025 at 5:16 PM John from Shaneforwoot < shaneforwoot@gmail.com> wrote:</p>
|
||||||
|
<blockquote>
|
||||||
|
<p>Hey</p>
|
||||||
|
<p>On Mon, Sep 29, 2025 at 4:59 PM John shivam@chatwoot.com wrote:</p>
|
||||||
|
<p>This is another quoted quoted text reply</p>
|
||||||
|
<p>This is nice</p>
|
||||||
|
<p>On Mon, Sep 29, 2025 at 4:21 PM John from Shaneforwoot < > shaneforwoot@gmail.com> wrote:</p>
|
||||||
|
<p>Hey there, this is a reply from Chatwoot, notice the quoted text</p>
|
||||||
|
<p>Hey there</p>
|
||||||
|
<p>This is an email text, enjoy reading this</p>
|
||||||
|
<p>-- Shivam Mishra, Chatwoot</p>
|
||||||
|
</blockquote>
|
||||||
|
</blockquote>
|
||||||
|
</blockquote>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const EMAIL_WITH_SIGNATURE = `
|
||||||
|
<p>Latest reply here.</p>
|
||||||
|
<p>Thanks,</p>
|
||||||
|
<p>Jane Doe</p>
|
||||||
|
<blockquote>
|
||||||
|
<p>On Mon, Sep 22, Someone wrote:</p>
|
||||||
|
<p>Previous reply content</p>
|
||||||
|
</blockquote>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const EMAIL_WITH_FOLLOW_UP_CONTENT = `
|
||||||
|
<blockquote>
|
||||||
|
<p>Inline quote that should stay</p>
|
||||||
|
</blockquote>
|
||||||
|
<p>Internal note follows</p>
|
||||||
|
<p>Regards,</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
describe('EmailQuoteExtractor', () => {
|
||||||
|
it('removes blockquote-based quotes from the email body', () => {
|
||||||
|
const cleanedHtml = EmailQuoteExtractor.extractQuotes(SAMPLE_EMAIL_HTML);
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.innerHTML = cleanedHtml;
|
||||||
|
|
||||||
|
expect(container.querySelectorAll('blockquote').length).toBe(0);
|
||||||
|
expect(container.textContent?.trim()).toBe('method');
|
||||||
|
expect(container.textContent).not.toContain(
|
||||||
|
'On Mon, Sep 29, 2025 at 5:18 PM'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps blockquote fallback when it is not the last top-level element', () => {
|
||||||
|
const cleanedHtml = EmailQuoteExtractor.extractQuotes(
|
||||||
|
EMAIL_WITH_FOLLOW_UP_CONTENT
|
||||||
|
);
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.innerHTML = cleanedHtml;
|
||||||
|
|
||||||
|
expect(container.querySelector('blockquote')).not.toBeNull();
|
||||||
|
expect(container.lastElementChild?.tagName).toBe('P');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('detects quote indicators in nested blockquotes', () => {
|
||||||
|
const result = EmailQuoteExtractor.hasQuotes(SAMPLE_EMAIL_HTML);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not flag blockquotes that are followed by other elements', () => {
|
||||||
|
expect(EmailQuoteExtractor.hasQuotes(EMAIL_WITH_FOLLOW_UP_CONTENT)).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when no quote indicators are present', () => {
|
||||||
|
const html = '<p>Plain content</p>';
|
||||||
|
expect(EmailQuoteExtractor.hasQuotes(html)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes trailing blockquotes while preserving trailing signatures', () => {
|
||||||
|
const cleanedHtml = EmailQuoteExtractor.extractQuotes(EMAIL_WITH_SIGNATURE);
|
||||||
|
|
||||||
|
expect(cleanedHtml).toContain('<p>Thanks,</p>');
|
||||||
|
expect(cleanedHtml).toContain('<p>Jane Doe</p>');
|
||||||
|
expect(cleanedHtml).not.toContain('<blockquote');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('detects quotes for trailing blockquotes even when signatures follow text', () => {
|
||||||
|
expect(EmailQuoteExtractor.hasQuotes(EMAIL_WITH_SIGNATURE)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,441 @@
|
|||||||
|
import {
|
||||||
|
extractPlainTextFromHtml,
|
||||||
|
getEmailSenderName,
|
||||||
|
getEmailSenderEmail,
|
||||||
|
getEmailDate,
|
||||||
|
formatQuotedEmailDate,
|
||||||
|
getInboxEmail,
|
||||||
|
buildQuotedEmailHeader,
|
||||||
|
buildQuotedEmailHeaderFromContact,
|
||||||
|
buildQuotedEmailHeaderFromInbox,
|
||||||
|
formatQuotedTextAsBlockquote,
|
||||||
|
extractQuotedEmailText,
|
||||||
|
truncatePreviewText,
|
||||||
|
appendQuotedTextToMessage,
|
||||||
|
} from '../quotedEmailHelper';
|
||||||
|
|
||||||
|
describe('quotedEmailHelper', () => {
|
||||||
|
describe('extractPlainTextFromHtml', () => {
|
||||||
|
it('returns empty string for null or undefined', () => {
|
||||||
|
expect(extractPlainTextFromHtml(null)).toBe('');
|
||||||
|
expect(extractPlainTextFromHtml(undefined)).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('strips HTML tags and returns plain text', () => {
|
||||||
|
const html = '<p>Hello <strong>world</strong></p>';
|
||||||
|
const result = extractPlainTextFromHtml(html);
|
||||||
|
expect(result).toBe('Hello world');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles complex HTML structure', () => {
|
||||||
|
const html = '<div><p>Line 1</p><p>Line 2</p></div>';
|
||||||
|
const result = extractPlainTextFromHtml(html);
|
||||||
|
expect(result).toContain('Line 1');
|
||||||
|
expect(result).toContain('Line 2');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getEmailSenderName', () => {
|
||||||
|
it('returns sender name from lastEmail', () => {
|
||||||
|
const lastEmail = { sender: { name: 'John Doe' } };
|
||||||
|
const result = getEmailSenderName(lastEmail, {});
|
||||||
|
expect(result).toBe('John Doe');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns contact name if sender name not available', () => {
|
||||||
|
const lastEmail = { sender: {} };
|
||||||
|
const contact = { name: 'Jane Smith' };
|
||||||
|
const result = getEmailSenderName(lastEmail, contact);
|
||||||
|
expect(result).toBe('Jane Smith');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty string if neither available', () => {
|
||||||
|
const result = getEmailSenderName({}, {});
|
||||||
|
expect(result).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('trims whitespace from names', () => {
|
||||||
|
const lastEmail = { sender: { name: ' John Doe ' } };
|
||||||
|
const result = getEmailSenderName(lastEmail, {});
|
||||||
|
expect(result).toBe('John Doe');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getEmailSenderEmail', () => {
|
||||||
|
it('returns sender email from lastEmail', () => {
|
||||||
|
const lastEmail = { sender: { email: 'john@example.com' } };
|
||||||
|
const result = getEmailSenderEmail(lastEmail, {});
|
||||||
|
expect(result).toBe('john@example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns email from contentAttributes if sender email not available', () => {
|
||||||
|
const lastEmail = {
|
||||||
|
contentAttributes: {
|
||||||
|
email: { from: ['jane@example.com'] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result = getEmailSenderEmail(lastEmail, {});
|
||||||
|
expect(result).toBe('jane@example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns contact email as fallback', () => {
|
||||||
|
const lastEmail = {};
|
||||||
|
const contact = { email: 'contact@example.com' };
|
||||||
|
const result = getEmailSenderEmail(lastEmail, contact);
|
||||||
|
expect(result).toBe('contact@example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('trims whitespace from emails', () => {
|
||||||
|
const lastEmail = { sender: { email: ' john@example.com ' } };
|
||||||
|
const result = getEmailSenderEmail(lastEmail, {});
|
||||||
|
expect(result).toBe('john@example.com');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getEmailDate', () => {
|
||||||
|
it('returns parsed date from email metadata', () => {
|
||||||
|
const lastEmail = {
|
||||||
|
contentAttributes: {
|
||||||
|
email: { date: '2024-01-15T10:30:00Z' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result = getEmailDate(lastEmail);
|
||||||
|
expect(result).toBeInstanceOf(Date);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns date from created_at timestamp', () => {
|
||||||
|
const lastEmail = { created_at: 1705318200 };
|
||||||
|
const result = getEmailDate(lastEmail);
|
||||||
|
expect(result).toBeInstanceOf(Date);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles millisecond timestamps', () => {
|
||||||
|
const lastEmail = { created_at: 1705318200000 };
|
||||||
|
const result = getEmailDate(lastEmail);
|
||||||
|
expect(result).toBeInstanceOf(Date);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null if no valid date found', () => {
|
||||||
|
const result = getEmailDate({});
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatQuotedEmailDate', () => {
|
||||||
|
it('formats date correctly', () => {
|
||||||
|
const date = new Date('2024-01-15T10:30:00Z');
|
||||||
|
const result = formatQuotedEmailDate(date);
|
||||||
|
expect(result).toMatch(/Mon, Jan 15, 2024 at/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty string for invalid date', () => {
|
||||||
|
const result = formatQuotedEmailDate('invalid');
|
||||||
|
expect(result).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getInboxEmail', () => {
|
||||||
|
it('returns email from contentAttributes.email.to', () => {
|
||||||
|
const lastEmail = {
|
||||||
|
contentAttributes: {
|
||||||
|
email: { to: ['inbox@example.com'] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result = getInboxEmail(lastEmail, {});
|
||||||
|
expect(result).toBe('inbox@example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns inbox email as fallback', () => {
|
||||||
|
const lastEmail = {};
|
||||||
|
const inbox = { email: 'support@example.com' };
|
||||||
|
const result = getInboxEmail(lastEmail, inbox);
|
||||||
|
expect(result).toBe('support@example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty string if no email found', () => {
|
||||||
|
expect(getInboxEmail({}, {})).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('trims whitespace from emails', () => {
|
||||||
|
const lastEmail = {
|
||||||
|
contentAttributes: {
|
||||||
|
email: { to: [' inbox@example.com '] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result = getInboxEmail(lastEmail, {});
|
||||||
|
expect(result).toBe('inbox@example.com');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('buildQuotedEmailHeaderFromContact', () => {
|
||||||
|
it('builds complete header with name and email', () => {
|
||||||
|
const lastEmail = {
|
||||||
|
sender: { name: 'John Doe', email: 'john@example.com' },
|
||||||
|
contentAttributes: {
|
||||||
|
email: { date: '2024-01-15T10:30:00Z' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result = buildQuotedEmailHeaderFromContact(lastEmail, {});
|
||||||
|
expect(result).toContain('John Doe');
|
||||||
|
expect(result).toContain('john@example.com');
|
||||||
|
expect(result).toContain('wrote:');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds header without name if not available', () => {
|
||||||
|
const lastEmail = {
|
||||||
|
sender: { email: 'john@example.com' },
|
||||||
|
contentAttributes: {
|
||||||
|
email: { date: '2024-01-15T10:30:00Z' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result = buildQuotedEmailHeaderFromContact(lastEmail, {});
|
||||||
|
expect(result).toContain('<john@example.com>');
|
||||||
|
expect(result).not.toContain('undefined');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty string if missing required data', () => {
|
||||||
|
expect(buildQuotedEmailHeaderFromContact(null, {})).toBe('');
|
||||||
|
expect(buildQuotedEmailHeaderFromContact({}, {})).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('buildQuotedEmailHeaderFromInbox', () => {
|
||||||
|
it('builds complete header with inbox name and email', () => {
|
||||||
|
const lastEmail = {
|
||||||
|
contentAttributes: {
|
||||||
|
email: {
|
||||||
|
date: '2024-01-15T10:30:00Z',
|
||||||
|
to: ['support@example.com'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const inbox = { name: 'Support Team', email: 'support@example.com' };
|
||||||
|
const result = buildQuotedEmailHeaderFromInbox(lastEmail, inbox);
|
||||||
|
expect(result).toContain('Support Team');
|
||||||
|
expect(result).toContain('support@example.com');
|
||||||
|
expect(result).toContain('wrote:');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds header without name if not available', () => {
|
||||||
|
const lastEmail = {
|
||||||
|
contentAttributes: {
|
||||||
|
email: {
|
||||||
|
date: '2024-01-15T10:30:00Z',
|
||||||
|
to: ['inbox@example.com'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const inbox = { email: 'inbox@example.com' };
|
||||||
|
const result = buildQuotedEmailHeaderFromInbox(lastEmail, inbox);
|
||||||
|
expect(result).toContain('<inbox@example.com>');
|
||||||
|
expect(result).not.toContain('undefined');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty string if missing required data', () => {
|
||||||
|
expect(buildQuotedEmailHeaderFromInbox(null, {})).toBe('');
|
||||||
|
expect(buildQuotedEmailHeaderFromInbox({}, {})).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('buildQuotedEmailHeader', () => {
|
||||||
|
it('uses inbox email for outgoing messages (message_type: 1)', () => {
|
||||||
|
const lastEmail = {
|
||||||
|
message_type: 1,
|
||||||
|
contentAttributes: {
|
||||||
|
email: {
|
||||||
|
date: '2024-01-15T10:30:00Z',
|
||||||
|
to: ['support@example.com'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const inbox = { name: 'Support', email: 'support@example.com' };
|
||||||
|
const contact = { name: 'John Doe', email: 'john@example.com' };
|
||||||
|
const result = buildQuotedEmailHeader(lastEmail, contact, inbox);
|
||||||
|
expect(result).toContain('Support');
|
||||||
|
expect(result).toContain('support@example.com');
|
||||||
|
expect(result).not.toContain('John Doe');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses contact email for incoming messages (message_type: 0)', () => {
|
||||||
|
const lastEmail = {
|
||||||
|
message_type: 0,
|
||||||
|
sender: { name: 'Jane Smith', email: 'jane@example.com' },
|
||||||
|
contentAttributes: {
|
||||||
|
email: { date: '2024-01-15T10:30:00Z' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const inbox = { name: 'Support', email: 'support@example.com' };
|
||||||
|
const contact = { name: 'Jane Smith', email: 'jane@example.com' };
|
||||||
|
const result = buildQuotedEmailHeader(lastEmail, contact, inbox);
|
||||||
|
expect(result).toContain('Jane Smith');
|
||||||
|
expect(result).toContain('jane@example.com');
|
||||||
|
expect(result).not.toContain('Support');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty string if missing required data', () => {
|
||||||
|
expect(buildQuotedEmailHeader(null, {}, {})).toBe('');
|
||||||
|
expect(buildQuotedEmailHeader({}, {}, {})).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatQuotedTextAsBlockquote', () => {
|
||||||
|
it('formats single line text', () => {
|
||||||
|
const result = formatQuotedTextAsBlockquote('Hello world');
|
||||||
|
expect(result).toBe('> Hello world');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('formats multi-line text', () => {
|
||||||
|
const text = 'Line 1\nLine 2\nLine 3';
|
||||||
|
const result = formatQuotedTextAsBlockquote(text);
|
||||||
|
expect(result).toBe('> Line 1\n> Line 2\n> Line 3');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes header if provided', () => {
|
||||||
|
const result = formatQuotedTextAsBlockquote('Hello', 'Header text');
|
||||||
|
expect(result).toContain('> Header text');
|
||||||
|
expect(result).toContain('>\n> Hello');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles empty lines correctly', () => {
|
||||||
|
const text = 'Line 1\n\nLine 3';
|
||||||
|
const result = formatQuotedTextAsBlockquote(text);
|
||||||
|
expect(result).toBe('> Line 1\n>\n> Line 3');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty string for empty input', () => {
|
||||||
|
expect(formatQuotedTextAsBlockquote('')).toBe('');
|
||||||
|
expect(formatQuotedTextAsBlockquote('', '')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles Windows line endings', () => {
|
||||||
|
const text = 'Line 1\r\nLine 2';
|
||||||
|
const result = formatQuotedTextAsBlockquote(text);
|
||||||
|
expect(result).toBe('> Line 1\n> Line 2');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('extractQuotedEmailText', () => {
|
||||||
|
it('extracts text from textContent.reply', () => {
|
||||||
|
const lastEmail = {
|
||||||
|
contentAttributes: {
|
||||||
|
email: { textContent: { reply: 'Reply text' } },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result = extractQuotedEmailText(lastEmail);
|
||||||
|
expect(result).toBe('Reply text');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to textContent.full', () => {
|
||||||
|
const lastEmail = {
|
||||||
|
contentAttributes: {
|
||||||
|
email: { textContent: { full: 'Full text' } },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result = extractQuotedEmailText(lastEmail);
|
||||||
|
expect(result).toBe('Full text');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('extracts from htmlContent and converts to plain text', () => {
|
||||||
|
const lastEmail = {
|
||||||
|
contentAttributes: {
|
||||||
|
email: { htmlContent: { reply: '<p>HTML reply</p>' } },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result = extractQuotedEmailText(lastEmail);
|
||||||
|
expect(result).toBe('HTML reply');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses fallback content if structured content not available', () => {
|
||||||
|
const lastEmail = { content: 'Fallback content' };
|
||||||
|
const result = extractQuotedEmailText(lastEmail);
|
||||||
|
expect(result).toBe('Fallback content');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty string for null or missing email', () => {
|
||||||
|
expect(extractQuotedEmailText(null)).toBe('');
|
||||||
|
expect(extractQuotedEmailText({})).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('truncatePreviewText', () => {
|
||||||
|
it('returns full text if under max length', () => {
|
||||||
|
const text = 'Short text';
|
||||||
|
const result = truncatePreviewText(text, 80);
|
||||||
|
expect(result).toBe('Short text');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('truncates text exceeding max length', () => {
|
||||||
|
const text = 'A'.repeat(100);
|
||||||
|
const result = truncatePreviewText(text, 80);
|
||||||
|
expect(result).toHaveLength(80);
|
||||||
|
expect(result).toContain('...');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('collapses multiple spaces', () => {
|
||||||
|
const text = 'Text with spaces';
|
||||||
|
const result = truncatePreviewText(text);
|
||||||
|
expect(result).toBe('Text with spaces');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('trims whitespace', () => {
|
||||||
|
const text = ' Text with spaces ';
|
||||||
|
const result = truncatePreviewText(text);
|
||||||
|
expect(result).toBe('Text with spaces');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty string for empty input', () => {
|
||||||
|
expect(truncatePreviewText('')).toBe('');
|
||||||
|
expect(truncatePreviewText(' ')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses default max length of 80', () => {
|
||||||
|
const text = 'A'.repeat(100);
|
||||||
|
const result = truncatePreviewText(text);
|
||||||
|
expect(result).toHaveLength(80);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('appendQuotedTextToMessage', () => {
|
||||||
|
it('appends quoted text to message', () => {
|
||||||
|
const message = 'My reply';
|
||||||
|
const quotedText = 'Original message';
|
||||||
|
const header = 'On date sender wrote:';
|
||||||
|
const result = appendQuotedTextToMessage(message, quotedText, header);
|
||||||
|
|
||||||
|
expect(result).toContain('My reply');
|
||||||
|
expect(result).toContain('> On date sender wrote:');
|
||||||
|
expect(result).toContain('> Original message');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns only quoted text if message is empty', () => {
|
||||||
|
const result = appendQuotedTextToMessage('', 'Quoted', 'Header');
|
||||||
|
expect(result).toContain('> Header');
|
||||||
|
expect(result).toContain('> Quoted');
|
||||||
|
expect(result).not.toContain('\n\n\n');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns message if no quoted text', () => {
|
||||||
|
const result = appendQuotedTextToMessage('Message', '', '');
|
||||||
|
expect(result).toBe('Message');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles proper spacing with double newline', () => {
|
||||||
|
const result = appendQuotedTextToMessage('Message', 'Quoted', 'Header');
|
||||||
|
expect(result).toContain('Message\n\n>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not add extra newlines if message already ends with newlines', () => {
|
||||||
|
const result = appendQuotedTextToMessage(
|
||||||
|
'Message\n\n',
|
||||||
|
'Quoted',
|
||||||
|
'Header'
|
||||||
|
);
|
||||||
|
expect(result).not.toContain('\n\n\n');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds single newline if message ends with one newline', () => {
|
||||||
|
const result = appendQuotedTextToMessage('Message\n', 'Quoted', 'Header');
|
||||||
|
expect(result).toContain('Message\n\n>');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -554,10 +554,12 @@
|
|||||||
"WROTE": "wrote",
|
"WROTE": "wrote",
|
||||||
"YOU": "You",
|
"YOU": "You",
|
||||||
"SAVE": "Save note",
|
"SAVE": "Save note",
|
||||||
|
"ADD_NOTE": "Add contact note",
|
||||||
"EXPAND": "Expand",
|
"EXPAND": "Expand",
|
||||||
"COLLAPSE": "ሰብስብ",
|
"COLLAPSE": "ሰብስብ",
|
||||||
"NO_NOTES": "ማስታወሻዎች የሉም፣ ከእውቂያው ዝርዝር ገፅ ላይ ማስታወሻዎችን መጨመር ይችላሉ።",
|
"NO_NOTES": "ማስታወሻዎች የሉም፣ ከእውቂያው ዝርዝር ገፅ ላይ ማስታወሻዎችን መጨመር ይችላሉ።",
|
||||||
"EMPTY_STATE": "There are no notes associated to this contact. You can add a note by typing in the box above."
|
"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."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"EMPTY_STATE": {
|
"EMPTY_STATE": {
|
||||||
|
|||||||
@@ -227,6 +227,13 @@
|
|||||||
"YES": "Send",
|
"YES": "Send",
|
||||||
"CANCEL": "Cancel"
|
"CANCEL": "Cancel"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team",
|
"VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team",
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
"PLACEHOLDER": "Search",
|
"PLACEHOLDER": "Search",
|
||||||
"EMPTY_STATE": "No results found"
|
"EMPTY_STATE": "No results found"
|
||||||
},
|
},
|
||||||
"CLOSE": "Close"
|
"CLOSE": "Close",
|
||||||
|
"BETA": "Beta",
|
||||||
|
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
"LEARN_MORE": "Learn more about inboxes",
|
"LEARN_MORE": "Learn more about inboxes",
|
||||||
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
|
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
|
||||||
"CLICK_TO_RECONNECT": "Click here to reconnect.",
|
"CLICK_TO_RECONNECT": "Click here to reconnect.",
|
||||||
|
"WHATSAPP_REGISTRATION_INCOMPLETE": "Your WhatsApp Business registration isn’t complete. Please check your display name status in Meta Business Manager before reconnecting.",
|
||||||
|
"COMPLETE_REGISTRATION": "Complete Registration",
|
||||||
"LIST": {
|
"LIST": {
|
||||||
"404": "There are no inboxes attached to this account."
|
"404": "There are no inboxes attached to this account."
|
||||||
},
|
},
|
||||||
@@ -605,8 +607,62 @@
|
|||||||
"BUSINESS_HOURS": "Business Hours",
|
"BUSINESS_HOURS": "Business Hours",
|
||||||
"WIDGET_BUILDER": "Widget Builder",
|
"WIDGET_BUILDER": "Widget Builder",
|
||||||
"BOT_CONFIGURATION": "Bot Configuration",
|
"BOT_CONFIGURATION": "Bot Configuration",
|
||||||
|
"ACCOUNT_HEALTH": "Account Health",
|
||||||
"CSAT": "CSAT"
|
"CSAT": "CSAT"
|
||||||
},
|
},
|
||||||
|
"ACCOUNT_HEALTH": {
|
||||||
|
"TITLE": "Manage your WhatsApp account",
|
||||||
|
"DESCRIPTION": "Review your WhatsApp account status, messaging limits, and quality. Update settings or resolve issues if needed",
|
||||||
|
"GO_TO_SETTINGS": "Go to Meta Business Manager",
|
||||||
|
"NO_DATA": "Health data is not available",
|
||||||
|
"FIELDS": {
|
||||||
|
"DISPLAY_PHONE_NUMBER": {
|
||||||
|
"LABEL": "Display phone number",
|
||||||
|
"TOOLTIP": "Phone number displayed to customers"
|
||||||
|
},
|
||||||
|
"VERIFIED_NAME": {
|
||||||
|
"LABEL": "Business name",
|
||||||
|
"TOOLTIP": "Business name verified by WhatsApp"
|
||||||
|
},
|
||||||
|
"DISPLAY_NAME_STATUS": {
|
||||||
|
"LABEL": "Display name status",
|
||||||
|
"TOOLTIP": "Status of your business name verification"
|
||||||
|
},
|
||||||
|
"QUALITY_RATING": {
|
||||||
|
"LABEL": "Quality rating",
|
||||||
|
"TOOLTIP": "WhatsApp quality rating for your account"
|
||||||
|
},
|
||||||
|
"MESSAGING_LIMIT_TIER": {
|
||||||
|
"LABEL": "Messaging limit tier",
|
||||||
|
"TOOLTIP": "Daily messaging limit for your account"
|
||||||
|
},
|
||||||
|
"ACCOUNT_MODE": {
|
||||||
|
"LABEL": "Account mode",
|
||||||
|
"TOOLTIP": "Current operating mode of your WhatsApp account"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"VALUES": {
|
||||||
|
"TIERS": {
|
||||||
|
"TIER_250": "250 customers per 24h",
|
||||||
|
"TIER_1000": "1K customers per 24h",
|
||||||
|
"TIER_1K": "1K customers per 24h",
|
||||||
|
"TIER_10K": "10K customers per 24h",
|
||||||
|
"TIER_100K": "100K customers per 24h",
|
||||||
|
"TIER_UNLIMITED": "Unlimited customers per 24h"
|
||||||
|
},
|
||||||
|
"STATUSES": {
|
||||||
|
"APPROVED": "Approved",
|
||||||
|
"PENDING_REVIEW": "Pending Review",
|
||||||
|
"AVAILABLE_WITHOUT_REVIEW": "Available Without Review",
|
||||||
|
"REJECTED": "Rejected",
|
||||||
|
"DECLINED": "Declined"
|
||||||
|
},
|
||||||
|
"MODES": {
|
||||||
|
"SANDBOX": "Sandbox",
|
||||||
|
"LIVE": "Live"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"SETTINGS": "Settings",
|
"SETTINGS": "Settings",
|
||||||
"FEATURES": {
|
"FEATURES": {
|
||||||
"LABEL": "Features",
|
"LABEL": "Features",
|
||||||
@@ -618,6 +674,11 @@
|
|||||||
"SETTINGS_POPUP": {
|
"SETTINGS_POPUP": {
|
||||||
"MESSENGER_HEADING": "Messenger Script",
|
"MESSENGER_HEADING": "Messenger Script",
|
||||||
"MESSENGER_SUB_HEAD": "Place this button inside your body tag",
|
"MESSENGER_SUB_HEAD": "Place this button inside your body tag",
|
||||||
|
"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.",
|
||||||
|
"PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)"
|
||||||
|
},
|
||||||
"INBOX_AGENTS": "Agents",
|
"INBOX_AGENTS": "Agents",
|
||||||
"INBOX_AGENTS_SUB_TEXT": "Add or remove agents from this inbox",
|
"INBOX_AGENTS_SUB_TEXT": "Add or remove agents from this inbox",
|
||||||
"AGENT_ASSIGNMENT": "Conversation Assignment",
|
"AGENT_ASSIGNMENT": "Conversation Assignment",
|
||||||
|
|||||||
@@ -22,6 +22,20 @@
|
|||||||
},
|
},
|
||||||
"FORGOT_PASSWORD": "Forgot your password?",
|
"FORGOT_PASSWORD": "Forgot your password?",
|
||||||
"CREATE_NEW_ACCOUNT": "Create a new account",
|
"CREATE_NEW_ACCOUNT": "Create a new account",
|
||||||
"SUBMIT": "Login"
|
"SUBMIT": "Login",
|
||||||
|
"SAML": {
|
||||||
|
"LABEL": "Login via SSO",
|
||||||
|
"TITLE": "Initiate Single Sign-on (SSO)",
|
||||||
|
"SUBTITLE": "Enter your work email to access your organization",
|
||||||
|
"BACK_TO_LOGIN": "Login via Password",
|
||||||
|
"WORK_EMAIL": {
|
||||||
|
"LABEL": "Work Email",
|
||||||
|
"PLACEHOLDER": "Enter your work email"
|
||||||
|
},
|
||||||
|
"SUBMIT": "Continue with SSO",
|
||||||
|
"API": {
|
||||||
|
"ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,15 +27,20 @@
|
|||||||
"LABEL": "Password",
|
"LABEL": "Password",
|
||||||
"PLACEHOLDER": "Password",
|
"PLACEHOLDER": "Password",
|
||||||
"ERROR": "Password is too short.",
|
"ERROR": "Password is too short.",
|
||||||
"IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character."
|
"IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character.",
|
||||||
|
"REQUIREMENTS_LENGTH": "At least 6 characters long",
|
||||||
|
"REQUIREMENTS_UPPERCASE": "At least one uppercase letter",
|
||||||
|
"REQUIREMENTS_LOWERCASE": "At least one lowercase letter",
|
||||||
|
"REQUIREMENTS_NUMBER": "At least one number",
|
||||||
|
"REQUIREMENTS_SPECIAL": "At least one special character"
|
||||||
},
|
},
|
||||||
"CONFIRM_PASSWORD": {
|
"CONFIRM_PASSWORD": {
|
||||||
"LABEL": "Confirm password",
|
"LABEL": "Confirm password",
|
||||||
"PLACEHOLDER": "Confirm password",
|
"PLACEHOLDER": "Confirm password",
|
||||||
"ERROR": "Password doesnot match."
|
"ERROR": "Passwords do not match."
|
||||||
},
|
},
|
||||||
"API": {
|
"API": {
|
||||||
"SUCCESS_MESSAGE": "Registration Successfull",
|
"SUCCESS_MESSAGE": "Registration Successful",
|
||||||
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
|
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
|
||||||
},
|
},
|
||||||
"SUBMIT": "Create account",
|
"SUBMIT": "Create account",
|
||||||
|
|||||||
@@ -554,10 +554,12 @@
|
|||||||
"WROTE": "كتب",
|
"WROTE": "كتب",
|
||||||
"YOU": "أنت",
|
"YOU": "أنت",
|
||||||
"SAVE": "Save note",
|
"SAVE": "Save note",
|
||||||
|
"ADD_NOTE": "Add contact note",
|
||||||
"EXPAND": "Expand",
|
"EXPAND": "Expand",
|
||||||
"COLLAPSE": "Collapse",
|
"COLLAPSE": "Collapse",
|
||||||
"NO_NOTES": "No notes, you can add notes from the contact details page.",
|
"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."
|
"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."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"EMPTY_STATE": {
|
"EMPTY_STATE": {
|
||||||
|
|||||||
@@ -227,6 +227,13 @@
|
|||||||
"YES": "إرسال",
|
"YES": "إرسال",
|
||||||
"CANCEL": "إلغاء"
|
"CANCEL": "إلغاء"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"VISIBLE_TO_AGENTS": "ملاحظة خاصة: مرئية فقط لك ولأعضاء فريقك",
|
"VISIBLE_TO_AGENTS": "ملاحظة خاصة: مرئية فقط لك ولأعضاء فريقك",
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
"PLACEHOLDER": "بحث",
|
"PLACEHOLDER": "بحث",
|
||||||
"EMPTY_STATE": "لم يتم العثور على النتائج"
|
"EMPTY_STATE": "لم يتم العثور على النتائج"
|
||||||
},
|
},
|
||||||
"CLOSE": "أغلق"
|
"CLOSE": "أغلق",
|
||||||
|
"BETA": "تجريبي",
|
||||||
|
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
"LEARN_MORE": "تعلم المزيد عن صناديق البريد",
|
"LEARN_MORE": "تعلم المزيد عن صناديق البريد",
|
||||||
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
|
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
|
||||||
"CLICK_TO_RECONNECT": "Click here to reconnect.",
|
"CLICK_TO_RECONNECT": "Click here to reconnect.",
|
||||||
|
"WHATSAPP_REGISTRATION_INCOMPLETE": "Your WhatsApp Business registration isn’t complete. Please check your display name status in Meta Business Manager before reconnecting.",
|
||||||
|
"COMPLETE_REGISTRATION": "Complete Registration",
|
||||||
"LIST": {
|
"LIST": {
|
||||||
"404": "لا توجد صناديق وارد لقنوات تواصل مرتبطة بهذا الحساب."
|
"404": "لا توجد صناديق وارد لقنوات تواصل مرتبطة بهذا الحساب."
|
||||||
},
|
},
|
||||||
@@ -605,8 +607,62 @@
|
|||||||
"BUSINESS_HOURS": "ساعات العمل",
|
"BUSINESS_HOURS": "ساعات العمل",
|
||||||
"WIDGET_BUILDER": "منشئ اللايف شات",
|
"WIDGET_BUILDER": "منشئ اللايف شات",
|
||||||
"BOT_CONFIGURATION": "اعدادات البوت",
|
"BOT_CONFIGURATION": "اعدادات البوت",
|
||||||
|
"ACCOUNT_HEALTH": "Account Health",
|
||||||
"CSAT": "تقييم رضاء العملاء"
|
"CSAT": "تقييم رضاء العملاء"
|
||||||
},
|
},
|
||||||
|
"ACCOUNT_HEALTH": {
|
||||||
|
"TITLE": "Manage your WhatsApp account",
|
||||||
|
"DESCRIPTION": "Review your WhatsApp account status, messaging limits, and quality. Update settings or resolve issues if needed",
|
||||||
|
"GO_TO_SETTINGS": "Go to Meta Business Manager",
|
||||||
|
"NO_DATA": "Health data is not available",
|
||||||
|
"FIELDS": {
|
||||||
|
"DISPLAY_PHONE_NUMBER": {
|
||||||
|
"LABEL": "Display phone number",
|
||||||
|
"TOOLTIP": "Phone number displayed to customers"
|
||||||
|
},
|
||||||
|
"VERIFIED_NAME": {
|
||||||
|
"LABEL": "Business name",
|
||||||
|
"TOOLTIP": "Business name verified by WhatsApp"
|
||||||
|
},
|
||||||
|
"DISPLAY_NAME_STATUS": {
|
||||||
|
"LABEL": "Display name status",
|
||||||
|
"TOOLTIP": "Status of your business name verification"
|
||||||
|
},
|
||||||
|
"QUALITY_RATING": {
|
||||||
|
"LABEL": "Quality rating",
|
||||||
|
"TOOLTIP": "WhatsApp quality rating for your account"
|
||||||
|
},
|
||||||
|
"MESSAGING_LIMIT_TIER": {
|
||||||
|
"LABEL": "Messaging limit tier",
|
||||||
|
"TOOLTIP": "Daily messaging limit for your account"
|
||||||
|
},
|
||||||
|
"ACCOUNT_MODE": {
|
||||||
|
"LABEL": "Account mode",
|
||||||
|
"TOOLTIP": "Current operating mode of your WhatsApp account"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"VALUES": {
|
||||||
|
"TIERS": {
|
||||||
|
"TIER_250": "250 customers per 24h",
|
||||||
|
"TIER_1000": "1K customers per 24h",
|
||||||
|
"TIER_1K": "1K customers per 24h",
|
||||||
|
"TIER_10K": "10K customers per 24h",
|
||||||
|
"TIER_100K": "100K customers per 24h",
|
||||||
|
"TIER_UNLIMITED": "Unlimited customers per 24h"
|
||||||
|
},
|
||||||
|
"STATUSES": {
|
||||||
|
"APPROVED": "Approved",
|
||||||
|
"PENDING_REVIEW": "Pending Review",
|
||||||
|
"AVAILABLE_WITHOUT_REVIEW": "Available Without Review",
|
||||||
|
"REJECTED": "Rejected",
|
||||||
|
"DECLINED": "Declined"
|
||||||
|
},
|
||||||
|
"MODES": {
|
||||||
|
"SANDBOX": "Sandbox",
|
||||||
|
"LIVE": "مباشر"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"SETTINGS": "الإعدادات",
|
"SETTINGS": "الإعدادات",
|
||||||
"FEATURES": {
|
"FEATURES": {
|
||||||
"LABEL": "الخصائص",
|
"LABEL": "الخصائص",
|
||||||
@@ -618,6 +674,11 @@
|
|||||||
"SETTINGS_POPUP": {
|
"SETTINGS_POPUP": {
|
||||||
"MESSENGER_HEADING": "كود \"الماسنجر\"",
|
"MESSENGER_HEADING": "كود \"الماسنجر\"",
|
||||||
"MESSENGER_SUB_HEAD": "ضع هذا الكود داخل وسم الـ body في موقعك",
|
"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.",
|
||||||
|
"PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)"
|
||||||
|
},
|
||||||
"INBOX_AGENTS": "وكيل الدعم",
|
"INBOX_AGENTS": "وكيل الدعم",
|
||||||
"INBOX_AGENTS_SUB_TEXT": "إضافة أو إزالة وكلاء من صندوق الوارد هذا",
|
"INBOX_AGENTS_SUB_TEXT": "إضافة أو إزالة وكلاء من صندوق الوارد هذا",
|
||||||
"AGENT_ASSIGNMENT": "تعيين المحادثة",
|
"AGENT_ASSIGNMENT": "تعيين المحادثة",
|
||||||
|
|||||||
@@ -22,6 +22,20 @@
|
|||||||
},
|
},
|
||||||
"FORGOT_PASSWORD": "نسيت كلمة المرور؟",
|
"FORGOT_PASSWORD": "نسيت كلمة المرور؟",
|
||||||
"CREATE_NEW_ACCOUNT": "إنشاء حساب جديد",
|
"CREATE_NEW_ACCOUNT": "إنشاء حساب جديد",
|
||||||
"SUBMIT": "تسجيل الدخول"
|
"SUBMIT": "تسجيل الدخول",
|
||||||
|
"SAML": {
|
||||||
|
"LABEL": "Login via SSO",
|
||||||
|
"TITLE": "Initiate Single Sign-on (SSO)",
|
||||||
|
"SUBTITLE": "Enter your work email to access your organization",
|
||||||
|
"BACK_TO_LOGIN": "Login via Password",
|
||||||
|
"WORK_EMAIL": {
|
||||||
|
"LABEL": "Work Email",
|
||||||
|
"PLACEHOLDER": "Enter your work email"
|
||||||
|
},
|
||||||
|
"SUBMIT": "Continue with SSO",
|
||||||
|
"API": {
|
||||||
|
"ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,15 +27,20 @@
|
|||||||
"LABEL": "كلمة المرور",
|
"LABEL": "كلمة المرور",
|
||||||
"PLACEHOLDER": "كلمة المرور",
|
"PLACEHOLDER": "كلمة المرور",
|
||||||
"ERROR": "كلمة المرور قصيرة جداً",
|
"ERROR": "كلمة المرور قصيرة جداً",
|
||||||
"IS_INVALID_PASSWORD": "يجب أن تحتوي كلمة المرور على الأقل على حرف كبير واحد وحرف صغير واحد ورقم واحد وحرف خاص واحد"
|
"IS_INVALID_PASSWORD": "يجب أن تحتوي كلمة المرور على الأقل على حرف كبير واحد وحرف صغير واحد ورقم واحد وحرف خاص واحد",
|
||||||
|
"REQUIREMENTS_LENGTH": "At least 6 characters long",
|
||||||
|
"REQUIREMENTS_UPPERCASE": "At least one uppercase letter",
|
||||||
|
"REQUIREMENTS_LOWERCASE": "At least one lowercase letter",
|
||||||
|
"REQUIREMENTS_NUMBER": "At least one number",
|
||||||
|
"REQUIREMENTS_SPECIAL": "At least one special character"
|
||||||
},
|
},
|
||||||
"CONFIRM_PASSWORD": {
|
"CONFIRM_PASSWORD": {
|
||||||
"LABEL": "تأكيد كلمة المرور",
|
"LABEL": "تأكيد كلمة المرور",
|
||||||
"PLACEHOLDER": "تأكيد كلمة المرور",
|
"PLACEHOLDER": "تأكيد كلمة المرور",
|
||||||
"ERROR": "كلمة المرور غير متطابقة"
|
"ERROR": "كلمة المرور غير متطابقة."
|
||||||
},
|
},
|
||||||
"API": {
|
"API": {
|
||||||
"SUCCESS_MESSAGE": "تم التسجيل بنجاح",
|
"SUCCESS_MESSAGE": "Registration Successful",
|
||||||
"ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً"
|
"ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً"
|
||||||
},
|
},
|
||||||
"SUBMIT": "إرسال",
|
"SUBMIT": "إرسال",
|
||||||
|
|||||||
@@ -554,10 +554,12 @@
|
|||||||
"WROTE": "wrote",
|
"WROTE": "wrote",
|
||||||
"YOU": "You",
|
"YOU": "You",
|
||||||
"SAVE": "Save note",
|
"SAVE": "Save note",
|
||||||
|
"ADD_NOTE": "Add contact note",
|
||||||
"EXPAND": "Expand",
|
"EXPAND": "Expand",
|
||||||
"COLLAPSE": "Collapse",
|
"COLLAPSE": "Collapse",
|
||||||
"NO_NOTES": "No notes, you can add notes from the contact details page.",
|
"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."
|
"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."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"EMPTY_STATE": {
|
"EMPTY_STATE": {
|
||||||
|
|||||||
@@ -227,6 +227,13 @@
|
|||||||
"YES": "Send",
|
"YES": "Send",
|
||||||
"CANCEL": "Cancel"
|
"CANCEL": "Cancel"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team",
|
"VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team",
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
"PLACEHOLDER": "Search",
|
"PLACEHOLDER": "Search",
|
||||||
"EMPTY_STATE": "No results found"
|
"EMPTY_STATE": "No results found"
|
||||||
},
|
},
|
||||||
"CLOSE": "Close"
|
"CLOSE": "Close",
|
||||||
|
"BETA": "Beta",
|
||||||
|
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
"LEARN_MORE": "Learn more about inboxes",
|
"LEARN_MORE": "Learn more about inboxes",
|
||||||
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
|
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
|
||||||
"CLICK_TO_RECONNECT": "Click here to reconnect.",
|
"CLICK_TO_RECONNECT": "Click here to reconnect.",
|
||||||
|
"WHATSAPP_REGISTRATION_INCOMPLETE": "Your WhatsApp Business registration isn’t complete. Please check your display name status in Meta Business Manager before reconnecting.",
|
||||||
|
"COMPLETE_REGISTRATION": "Complete Registration",
|
||||||
"LIST": {
|
"LIST": {
|
||||||
"404": "There are no inboxes attached to this account."
|
"404": "There are no inboxes attached to this account."
|
||||||
},
|
},
|
||||||
@@ -605,8 +607,62 @@
|
|||||||
"BUSINESS_HOURS": "Business Hours",
|
"BUSINESS_HOURS": "Business Hours",
|
||||||
"WIDGET_BUILDER": "Widget Builder",
|
"WIDGET_BUILDER": "Widget Builder",
|
||||||
"BOT_CONFIGURATION": "Bot Configuration",
|
"BOT_CONFIGURATION": "Bot Configuration",
|
||||||
|
"ACCOUNT_HEALTH": "Account Health",
|
||||||
"CSAT": "CSAT"
|
"CSAT": "CSAT"
|
||||||
},
|
},
|
||||||
|
"ACCOUNT_HEALTH": {
|
||||||
|
"TITLE": "Manage your WhatsApp account",
|
||||||
|
"DESCRIPTION": "Review your WhatsApp account status, messaging limits, and quality. Update settings or resolve issues if needed",
|
||||||
|
"GO_TO_SETTINGS": "Go to Meta Business Manager",
|
||||||
|
"NO_DATA": "Health data is not available",
|
||||||
|
"FIELDS": {
|
||||||
|
"DISPLAY_PHONE_NUMBER": {
|
||||||
|
"LABEL": "Display phone number",
|
||||||
|
"TOOLTIP": "Phone number displayed to customers"
|
||||||
|
},
|
||||||
|
"VERIFIED_NAME": {
|
||||||
|
"LABEL": "Business name",
|
||||||
|
"TOOLTIP": "Business name verified by WhatsApp"
|
||||||
|
},
|
||||||
|
"DISPLAY_NAME_STATUS": {
|
||||||
|
"LABEL": "Display name status",
|
||||||
|
"TOOLTIP": "Status of your business name verification"
|
||||||
|
},
|
||||||
|
"QUALITY_RATING": {
|
||||||
|
"LABEL": "Quality rating",
|
||||||
|
"TOOLTIP": "WhatsApp quality rating for your account"
|
||||||
|
},
|
||||||
|
"MESSAGING_LIMIT_TIER": {
|
||||||
|
"LABEL": "Messaging limit tier",
|
||||||
|
"TOOLTIP": "Daily messaging limit for your account"
|
||||||
|
},
|
||||||
|
"ACCOUNT_MODE": {
|
||||||
|
"LABEL": "Account mode",
|
||||||
|
"TOOLTIP": "Current operating mode of your WhatsApp account"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"VALUES": {
|
||||||
|
"TIERS": {
|
||||||
|
"TIER_250": "250 customers per 24h",
|
||||||
|
"TIER_1000": "1K customers per 24h",
|
||||||
|
"TIER_1K": "1K customers per 24h",
|
||||||
|
"TIER_10K": "10K customers per 24h",
|
||||||
|
"TIER_100K": "100K customers per 24h",
|
||||||
|
"TIER_UNLIMITED": "Unlimited customers per 24h"
|
||||||
|
},
|
||||||
|
"STATUSES": {
|
||||||
|
"APPROVED": "Approved",
|
||||||
|
"PENDING_REVIEW": "Pending Review",
|
||||||
|
"AVAILABLE_WITHOUT_REVIEW": "Available Without Review",
|
||||||
|
"REJECTED": "Rejected",
|
||||||
|
"DECLINED": "Declined"
|
||||||
|
},
|
||||||
|
"MODES": {
|
||||||
|
"SANDBOX": "Sandbox",
|
||||||
|
"LIVE": "Live"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"SETTINGS": "Settings",
|
"SETTINGS": "Settings",
|
||||||
"FEATURES": {
|
"FEATURES": {
|
||||||
"LABEL": "Features",
|
"LABEL": "Features",
|
||||||
@@ -618,6 +674,11 @@
|
|||||||
"SETTINGS_POPUP": {
|
"SETTINGS_POPUP": {
|
||||||
"MESSENGER_HEADING": "Messenger Script",
|
"MESSENGER_HEADING": "Messenger Script",
|
||||||
"MESSENGER_SUB_HEAD": "Place this button inside your body tag",
|
"MESSENGER_SUB_HEAD": "Place this button inside your body tag",
|
||||||
|
"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.",
|
||||||
|
"PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)"
|
||||||
|
},
|
||||||
"INBOX_AGENTS": "Agents",
|
"INBOX_AGENTS": "Agents",
|
||||||
"INBOX_AGENTS_SUB_TEXT": "Add or remove agents from this inbox",
|
"INBOX_AGENTS_SUB_TEXT": "Add or remove agents from this inbox",
|
||||||
"AGENT_ASSIGNMENT": "Conversation Assignment",
|
"AGENT_ASSIGNMENT": "Conversation Assignment",
|
||||||
|
|||||||
@@ -22,6 +22,20 @@
|
|||||||
},
|
},
|
||||||
"FORGOT_PASSWORD": "Forgot your password?",
|
"FORGOT_PASSWORD": "Forgot your password?",
|
||||||
"CREATE_NEW_ACCOUNT": "Create a new account",
|
"CREATE_NEW_ACCOUNT": "Create a new account",
|
||||||
"SUBMIT": "Login"
|
"SUBMIT": "Login",
|
||||||
|
"SAML": {
|
||||||
|
"LABEL": "Login via SSO",
|
||||||
|
"TITLE": "Initiate Single Sign-on (SSO)",
|
||||||
|
"SUBTITLE": "Enter your work email to access your organization",
|
||||||
|
"BACK_TO_LOGIN": "Login via Password",
|
||||||
|
"WORK_EMAIL": {
|
||||||
|
"LABEL": "Work Email",
|
||||||
|
"PLACEHOLDER": "Enter your work email"
|
||||||
|
},
|
||||||
|
"SUBMIT": "Continue with SSO",
|
||||||
|
"API": {
|
||||||
|
"ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,15 +27,20 @@
|
|||||||
"LABEL": "Password",
|
"LABEL": "Password",
|
||||||
"PLACEHOLDER": "Password",
|
"PLACEHOLDER": "Password",
|
||||||
"ERROR": "Password is too short.",
|
"ERROR": "Password is too short.",
|
||||||
"IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character."
|
"IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character.",
|
||||||
|
"REQUIREMENTS_LENGTH": "At least 6 characters long",
|
||||||
|
"REQUIREMENTS_UPPERCASE": "At least one uppercase letter",
|
||||||
|
"REQUIREMENTS_LOWERCASE": "At least one lowercase letter",
|
||||||
|
"REQUIREMENTS_NUMBER": "At least one number",
|
||||||
|
"REQUIREMENTS_SPECIAL": "At least one special character"
|
||||||
},
|
},
|
||||||
"CONFIRM_PASSWORD": {
|
"CONFIRM_PASSWORD": {
|
||||||
"LABEL": "Confirm password",
|
"LABEL": "Confirm password",
|
||||||
"PLACEHOLDER": "Confirm password",
|
"PLACEHOLDER": "Confirm password",
|
||||||
"ERROR": "Password doesnot match."
|
"ERROR": "Passwords do not match."
|
||||||
},
|
},
|
||||||
"API": {
|
"API": {
|
||||||
"SUCCESS_MESSAGE": "Registration Successfull",
|
"SUCCESS_MESSAGE": "Registration Successful",
|
||||||
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
|
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
|
||||||
},
|
},
|
||||||
"SUBMIT": "Create account",
|
"SUBMIT": "Create account",
|
||||||
|
|||||||
@@ -554,10 +554,12 @@
|
|||||||
"WROTE": "wrote",
|
"WROTE": "wrote",
|
||||||
"YOU": "You",
|
"YOU": "You",
|
||||||
"SAVE": "Save note",
|
"SAVE": "Save note",
|
||||||
|
"ADD_NOTE": "Add contact note",
|
||||||
"EXPAND": "Expand",
|
"EXPAND": "Expand",
|
||||||
"COLLAPSE": "Collapse",
|
"COLLAPSE": "Collapse",
|
||||||
"NO_NOTES": "No notes, you can add notes from the contact details page.",
|
"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."
|
"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."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"EMPTY_STATE": {
|
"EMPTY_STATE": {
|
||||||
|
|||||||
@@ -227,6 +227,13 @@
|
|||||||
"YES": "Send",
|
"YES": "Send",
|
||||||
"CANCEL": "Отмени"
|
"CANCEL": "Отмени"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team",
|
"VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team",
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
"PLACEHOLDER": "Търсене",
|
"PLACEHOLDER": "Търсене",
|
||||||
"EMPTY_STATE": "Няма намерени резултати"
|
"EMPTY_STATE": "Няма намерени резултати"
|
||||||
},
|
},
|
||||||
"CLOSE": "Close"
|
"CLOSE": "Close",
|
||||||
|
"BETA": "Beta",
|
||||||
|
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
"LEARN_MORE": "Learn more about inboxes",
|
"LEARN_MORE": "Learn more about inboxes",
|
||||||
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
|
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
|
||||||
"CLICK_TO_RECONNECT": "Click here to reconnect.",
|
"CLICK_TO_RECONNECT": "Click here to reconnect.",
|
||||||
|
"WHATSAPP_REGISTRATION_INCOMPLETE": "Your WhatsApp Business registration isn’t complete. Please check your display name status in Meta Business Manager before reconnecting.",
|
||||||
|
"COMPLETE_REGISTRATION": "Complete Registration",
|
||||||
"LIST": {
|
"LIST": {
|
||||||
"404": "There are no inboxes attached to this account."
|
"404": "There are no inboxes attached to this account."
|
||||||
},
|
},
|
||||||
@@ -605,8 +607,62 @@
|
|||||||
"BUSINESS_HOURS": "Business Hours",
|
"BUSINESS_HOURS": "Business Hours",
|
||||||
"WIDGET_BUILDER": "Widget Builder",
|
"WIDGET_BUILDER": "Widget Builder",
|
||||||
"BOT_CONFIGURATION": "Bot Configuration",
|
"BOT_CONFIGURATION": "Bot Configuration",
|
||||||
|
"ACCOUNT_HEALTH": "Account Health",
|
||||||
"CSAT": "CSAT"
|
"CSAT": "CSAT"
|
||||||
},
|
},
|
||||||
|
"ACCOUNT_HEALTH": {
|
||||||
|
"TITLE": "Manage your WhatsApp account",
|
||||||
|
"DESCRIPTION": "Review your WhatsApp account status, messaging limits, and quality. Update settings or resolve issues if needed",
|
||||||
|
"GO_TO_SETTINGS": "Go to Meta Business Manager",
|
||||||
|
"NO_DATA": "Health data is not available",
|
||||||
|
"FIELDS": {
|
||||||
|
"DISPLAY_PHONE_NUMBER": {
|
||||||
|
"LABEL": "Display phone number",
|
||||||
|
"TOOLTIP": "Phone number displayed to customers"
|
||||||
|
},
|
||||||
|
"VERIFIED_NAME": {
|
||||||
|
"LABEL": "Business name",
|
||||||
|
"TOOLTIP": "Business name verified by WhatsApp"
|
||||||
|
},
|
||||||
|
"DISPLAY_NAME_STATUS": {
|
||||||
|
"LABEL": "Display name status",
|
||||||
|
"TOOLTIP": "Status of your business name verification"
|
||||||
|
},
|
||||||
|
"QUALITY_RATING": {
|
||||||
|
"LABEL": "Quality rating",
|
||||||
|
"TOOLTIP": "WhatsApp quality rating for your account"
|
||||||
|
},
|
||||||
|
"MESSAGING_LIMIT_TIER": {
|
||||||
|
"LABEL": "Messaging limit tier",
|
||||||
|
"TOOLTIP": "Daily messaging limit for your account"
|
||||||
|
},
|
||||||
|
"ACCOUNT_MODE": {
|
||||||
|
"LABEL": "Account mode",
|
||||||
|
"TOOLTIP": "Current operating mode of your WhatsApp account"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"VALUES": {
|
||||||
|
"TIERS": {
|
||||||
|
"TIER_250": "250 customers per 24h",
|
||||||
|
"TIER_1000": "1K customers per 24h",
|
||||||
|
"TIER_1K": "1K customers per 24h",
|
||||||
|
"TIER_10K": "10K customers per 24h",
|
||||||
|
"TIER_100K": "100K customers per 24h",
|
||||||
|
"TIER_UNLIMITED": "Unlimited customers per 24h"
|
||||||
|
},
|
||||||
|
"STATUSES": {
|
||||||
|
"APPROVED": "Approved",
|
||||||
|
"PENDING_REVIEW": "Pending Review",
|
||||||
|
"AVAILABLE_WITHOUT_REVIEW": "Available Without Review",
|
||||||
|
"REJECTED": "Rejected",
|
||||||
|
"DECLINED": "Declined"
|
||||||
|
},
|
||||||
|
"MODES": {
|
||||||
|
"SANDBOX": "Sandbox",
|
||||||
|
"LIVE": "Live"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"SETTINGS": "Settings",
|
"SETTINGS": "Settings",
|
||||||
"FEATURES": {
|
"FEATURES": {
|
||||||
"LABEL": "Features",
|
"LABEL": "Features",
|
||||||
@@ -618,6 +674,11 @@
|
|||||||
"SETTINGS_POPUP": {
|
"SETTINGS_POPUP": {
|
||||||
"MESSENGER_HEADING": "Messenger Script",
|
"MESSENGER_HEADING": "Messenger Script",
|
||||||
"MESSENGER_SUB_HEAD": "Place this button inside your body tag",
|
"MESSENGER_SUB_HEAD": "Place this button inside your body tag",
|
||||||
|
"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.",
|
||||||
|
"PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)"
|
||||||
|
},
|
||||||
"INBOX_AGENTS": "Агенти",
|
"INBOX_AGENTS": "Агенти",
|
||||||
"INBOX_AGENTS_SUB_TEXT": "Add or remove agents from this inbox",
|
"INBOX_AGENTS_SUB_TEXT": "Add or remove agents from this inbox",
|
||||||
"AGENT_ASSIGNMENT": "Conversation Assignment",
|
"AGENT_ASSIGNMENT": "Conversation Assignment",
|
||||||
|
|||||||
@@ -22,6 +22,20 @@
|
|||||||
},
|
},
|
||||||
"FORGOT_PASSWORD": "Forgot your password?",
|
"FORGOT_PASSWORD": "Forgot your password?",
|
||||||
"CREATE_NEW_ACCOUNT": "Create new account",
|
"CREATE_NEW_ACCOUNT": "Create new account",
|
||||||
"SUBMIT": "Login"
|
"SUBMIT": "Login",
|
||||||
|
"SAML": {
|
||||||
|
"LABEL": "Login via SSO",
|
||||||
|
"TITLE": "Initiate Single Sign-on (SSO)",
|
||||||
|
"SUBTITLE": "Enter your work email to access your organization",
|
||||||
|
"BACK_TO_LOGIN": "Login via Password",
|
||||||
|
"WORK_EMAIL": {
|
||||||
|
"LABEL": "Work Email",
|
||||||
|
"PLACEHOLDER": "Enter your work email"
|
||||||
|
},
|
||||||
|
"SUBMIT": "Continue with SSO",
|
||||||
|
"API": {
|
||||||
|
"ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,15 +27,20 @@
|
|||||||
"LABEL": "Password",
|
"LABEL": "Password",
|
||||||
"PLACEHOLDER": "Password",
|
"PLACEHOLDER": "Password",
|
||||||
"ERROR": "Password is too short",
|
"ERROR": "Password is too short",
|
||||||
"IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character"
|
"IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character",
|
||||||
|
"REQUIREMENTS_LENGTH": "At least 6 characters long",
|
||||||
|
"REQUIREMENTS_UPPERCASE": "At least one uppercase letter",
|
||||||
|
"REQUIREMENTS_LOWERCASE": "At least one lowercase letter",
|
||||||
|
"REQUIREMENTS_NUMBER": "At least one number",
|
||||||
|
"REQUIREMENTS_SPECIAL": "At least one special character"
|
||||||
},
|
},
|
||||||
"CONFIRM_PASSWORD": {
|
"CONFIRM_PASSWORD": {
|
||||||
"LABEL": "Confirm Password",
|
"LABEL": "Confirm Password",
|
||||||
"PLACEHOLDER": "Confirm Password",
|
"PLACEHOLDER": "Confirm Password",
|
||||||
"ERROR": "Password doesnot match"
|
"ERROR": "Passwords do not match."
|
||||||
},
|
},
|
||||||
"API": {
|
"API": {
|
||||||
"SUCCESS_MESSAGE": "Registration Successfull",
|
"SUCCESS_MESSAGE": "Registration Successful",
|
||||||
"ERROR_MESSAGE": "Не можа да се свърже с Woot сървър. Моля, опитайте отново по-късно"
|
"ERROR_MESSAGE": "Не можа да се свърже с Woot сървър. Моля, опитайте отново по-късно"
|
||||||
},
|
},
|
||||||
"SUBMIT": "Create account",
|
"SUBMIT": "Create account",
|
||||||
|
|||||||
@@ -554,10 +554,12 @@
|
|||||||
"WROTE": "va escriure",
|
"WROTE": "va escriure",
|
||||||
"YOU": "Tu",
|
"YOU": "Tu",
|
||||||
"SAVE": "Save note",
|
"SAVE": "Save note",
|
||||||
|
"ADD_NOTE": "Add contact note",
|
||||||
"EXPAND": "Expandeix",
|
"EXPAND": "Expandeix",
|
||||||
"COLLAPSE": "Collapse",
|
"COLLAPSE": "Collapse",
|
||||||
"NO_NOTES": "No notes, you can add notes from the contact details page.",
|
"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."
|
"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."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"EMPTY_STATE": {
|
"EMPTY_STATE": {
|
||||||
|
|||||||
@@ -227,6 +227,13 @@
|
|||||||
"YES": "Envia",
|
"YES": "Envia",
|
||||||
"CANCEL": "Cancel·la"
|
"CANCEL": "Cancel·la"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"VISIBLE_TO_AGENTS": "Nota privada: Només és visible per tu i el vostre equip",
|
"VISIBLE_TO_AGENTS": "Nota privada: Només és visible per tu i el vostre equip",
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
"PLACEHOLDER": "Cercar",
|
"PLACEHOLDER": "Cercar",
|
||||||
"EMPTY_STATE": "No s'ha trobat agents"
|
"EMPTY_STATE": "No s'ha trobat agents"
|
||||||
},
|
},
|
||||||
"CLOSE": "Tanca"
|
"CLOSE": "Tanca",
|
||||||
|
"BETA": "Beta",
|
||||||
|
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
"LEARN_MORE": "Learn more about inboxes",
|
"LEARN_MORE": "Learn more about inboxes",
|
||||||
"RECONNECTION_REQUIRED": "La teva safata d'entrada està desconnectada. No rebràs missatges nous fins que no els tornis a autoritzar.",
|
"RECONNECTION_REQUIRED": "La teva safata d'entrada està desconnectada. No rebràs missatges nous fins que no els tornis a autoritzar.",
|
||||||
"CLICK_TO_RECONNECT": "Fes clic aquí per tornar a connectar-te.",
|
"CLICK_TO_RECONNECT": "Fes clic aquí per tornar a connectar-te.",
|
||||||
|
"WHATSAPP_REGISTRATION_INCOMPLETE": "Your WhatsApp Business registration isn’t complete. Please check your display name status in Meta Business Manager before reconnecting.",
|
||||||
|
"COMPLETE_REGISTRATION": "Complete Registration",
|
||||||
"LIST": {
|
"LIST": {
|
||||||
"404": "No hi ha cap safata d'entrada connectat a aquest compte."
|
"404": "No hi ha cap safata d'entrada connectat a aquest compte."
|
||||||
},
|
},
|
||||||
@@ -605,8 +607,62 @@
|
|||||||
"BUSINESS_HOURS": "Horari comercial",
|
"BUSINESS_HOURS": "Horari comercial",
|
||||||
"WIDGET_BUILDER": "Creador del widget",
|
"WIDGET_BUILDER": "Creador del widget",
|
||||||
"BOT_CONFIGURATION": "Configuracions del bot",
|
"BOT_CONFIGURATION": "Configuracions del bot",
|
||||||
|
"ACCOUNT_HEALTH": "Account Health",
|
||||||
"CSAT": "CSAT"
|
"CSAT": "CSAT"
|
||||||
},
|
},
|
||||||
|
"ACCOUNT_HEALTH": {
|
||||||
|
"TITLE": "Manage your WhatsApp account",
|
||||||
|
"DESCRIPTION": "Review your WhatsApp account status, messaging limits, and quality. Update settings or resolve issues if needed",
|
||||||
|
"GO_TO_SETTINGS": "Go to Meta Business Manager",
|
||||||
|
"NO_DATA": "Health data is not available",
|
||||||
|
"FIELDS": {
|
||||||
|
"DISPLAY_PHONE_NUMBER": {
|
||||||
|
"LABEL": "Display phone number",
|
||||||
|
"TOOLTIP": "Phone number displayed to customers"
|
||||||
|
},
|
||||||
|
"VERIFIED_NAME": {
|
||||||
|
"LABEL": "Business name",
|
||||||
|
"TOOLTIP": "Business name verified by WhatsApp"
|
||||||
|
},
|
||||||
|
"DISPLAY_NAME_STATUS": {
|
||||||
|
"LABEL": "Display name status",
|
||||||
|
"TOOLTIP": "Status of your business name verification"
|
||||||
|
},
|
||||||
|
"QUALITY_RATING": {
|
||||||
|
"LABEL": "Quality rating",
|
||||||
|
"TOOLTIP": "WhatsApp quality rating for your account"
|
||||||
|
},
|
||||||
|
"MESSAGING_LIMIT_TIER": {
|
||||||
|
"LABEL": "Messaging limit tier",
|
||||||
|
"TOOLTIP": "Daily messaging limit for your account"
|
||||||
|
},
|
||||||
|
"ACCOUNT_MODE": {
|
||||||
|
"LABEL": "Account mode",
|
||||||
|
"TOOLTIP": "Current operating mode of your WhatsApp account"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"VALUES": {
|
||||||
|
"TIERS": {
|
||||||
|
"TIER_250": "250 customers per 24h",
|
||||||
|
"TIER_1000": "1K customers per 24h",
|
||||||
|
"TIER_1K": "1K customers per 24h",
|
||||||
|
"TIER_10K": "10K customers per 24h",
|
||||||
|
"TIER_100K": "100K customers per 24h",
|
||||||
|
"TIER_UNLIMITED": "Unlimited customers per 24h"
|
||||||
|
},
|
||||||
|
"STATUSES": {
|
||||||
|
"APPROVED": "Approved",
|
||||||
|
"PENDING_REVIEW": "Pending Review",
|
||||||
|
"AVAILABLE_WITHOUT_REVIEW": "Available Without Review",
|
||||||
|
"REJECTED": "Rejected",
|
||||||
|
"DECLINED": "Declined"
|
||||||
|
},
|
||||||
|
"MODES": {
|
||||||
|
"SANDBOX": "Sandbox",
|
||||||
|
"LIVE": "En directe"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"SETTINGS": "Configuracions",
|
"SETTINGS": "Configuracions",
|
||||||
"FEATURES": {
|
"FEATURES": {
|
||||||
"LABEL": "Característiques",
|
"LABEL": "Característiques",
|
||||||
@@ -618,6 +674,11 @@
|
|||||||
"SETTINGS_POPUP": {
|
"SETTINGS_POPUP": {
|
||||||
"MESSENGER_HEADING": "Script del missatger",
|
"MESSENGER_HEADING": "Script del missatger",
|
||||||
"MESSENGER_SUB_HEAD": "Col·loca aquest botó dins de l'etiqueta body",
|
"MESSENGER_SUB_HEAD": "Col·loca aquest botó dins de l'etiqueta 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.",
|
||||||
|
"PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)"
|
||||||
|
},
|
||||||
"INBOX_AGENTS": "Agents",
|
"INBOX_AGENTS": "Agents",
|
||||||
"INBOX_AGENTS_SUB_TEXT": "Afegir o eliminar agents d'aquesta safata d'entrada",
|
"INBOX_AGENTS_SUB_TEXT": "Afegir o eliminar agents d'aquesta safata d'entrada",
|
||||||
"AGENT_ASSIGNMENT": "Conversació Assignada",
|
"AGENT_ASSIGNMENT": "Conversació Assignada",
|
||||||
|
|||||||
@@ -22,6 +22,20 @@
|
|||||||
},
|
},
|
||||||
"FORGOT_PASSWORD": "Has oblidat la contrasenya?",
|
"FORGOT_PASSWORD": "Has oblidat la contrasenya?",
|
||||||
"CREATE_NEW_ACCOUNT": "Crear un nou compte",
|
"CREATE_NEW_ACCOUNT": "Crear un nou compte",
|
||||||
"SUBMIT": "Inicia la sessió"
|
"SUBMIT": "Inicia la sessió",
|
||||||
|
"SAML": {
|
||||||
|
"LABEL": "Login via SSO",
|
||||||
|
"TITLE": "Initiate Single Sign-on (SSO)",
|
||||||
|
"SUBTITLE": "Enter your work email to access your organization",
|
||||||
|
"BACK_TO_LOGIN": "Login via Password",
|
||||||
|
"WORK_EMAIL": {
|
||||||
|
"LABEL": "Work Email",
|
||||||
|
"PLACEHOLDER": "Enter your work email"
|
||||||
|
},
|
||||||
|
"SUBMIT": "Continue with SSO",
|
||||||
|
"API": {
|
||||||
|
"ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,15 +27,20 @@
|
|||||||
"LABEL": "Contrasenya",
|
"LABEL": "Contrasenya",
|
||||||
"PLACEHOLDER": "Contrasenya",
|
"PLACEHOLDER": "Contrasenya",
|
||||||
"ERROR": "La contrasenya és massa curta",
|
"ERROR": "La contrasenya és massa curta",
|
||||||
"IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character"
|
"IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character",
|
||||||
|
"REQUIREMENTS_LENGTH": "At least 6 characters long",
|
||||||
|
"REQUIREMENTS_UPPERCASE": "At least one uppercase letter",
|
||||||
|
"REQUIREMENTS_LOWERCASE": "At least one lowercase letter",
|
||||||
|
"REQUIREMENTS_NUMBER": "At least one number",
|
||||||
|
"REQUIREMENTS_SPECIAL": "At least one special character"
|
||||||
},
|
},
|
||||||
"CONFIRM_PASSWORD": {
|
"CONFIRM_PASSWORD": {
|
||||||
"LABEL": "Confirma la contrasenya",
|
"LABEL": "Confirma la contrasenya",
|
||||||
"PLACEHOLDER": "Confirma la contrasenya",
|
"PLACEHOLDER": "Confirma la contrasenya",
|
||||||
"ERROR": "La contrasenya no coincideix."
|
"ERROR": "La contrasenya no coindeix."
|
||||||
},
|
},
|
||||||
"API": {
|
"API": {
|
||||||
"SUCCESS_MESSAGE": "Registrat correctament",
|
"SUCCESS_MESSAGE": "Registration Successful",
|
||||||
"ERROR_MESSAGE": "No s'ha pogut connectar amb el servidor Woot. Torna-ho a provar més endavant"
|
"ERROR_MESSAGE": "No s'ha pogut connectar amb el servidor Woot. Torna-ho a provar més endavant"
|
||||||
},
|
},
|
||||||
"SUBMIT": "Crear un compte",
|
"SUBMIT": "Crear un compte",
|
||||||
|
|||||||
@@ -554,10 +554,12 @@
|
|||||||
"WROTE": "wrote",
|
"WROTE": "wrote",
|
||||||
"YOU": "Vy",
|
"YOU": "Vy",
|
||||||
"SAVE": "Save note",
|
"SAVE": "Save note",
|
||||||
|
"ADD_NOTE": "Add contact note",
|
||||||
"EXPAND": "Expand",
|
"EXPAND": "Expand",
|
||||||
"COLLAPSE": "Collapse",
|
"COLLAPSE": "Collapse",
|
||||||
"NO_NOTES": "No notes, you can add notes from the contact details page.",
|
"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."
|
"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."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"EMPTY_STATE": {
|
"EMPTY_STATE": {
|
||||||
|
|||||||
@@ -227,6 +227,13 @@
|
|||||||
"YES": "Poslat",
|
"YES": "Poslat",
|
||||||
"CANCEL": "Zrušit"
|
"CANCEL": "Zrušit"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"VISIBLE_TO_AGENTS": "Soukromá poznámka: Viditelné pouze pro vás a váš tým",
|
"VISIBLE_TO_AGENTS": "Soukromá poznámka: Viditelné pouze pro vás a váš tým",
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
"PLACEHOLDER": "Hledat",
|
"PLACEHOLDER": "Hledat",
|
||||||
"EMPTY_STATE": "Žádné výsledky"
|
"EMPTY_STATE": "Žádné výsledky"
|
||||||
},
|
},
|
||||||
"CLOSE": "Zavřít"
|
"CLOSE": "Zavřít",
|
||||||
|
"BETA": "Beta",
|
||||||
|
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
"LEARN_MORE": "Learn more about inboxes",
|
"LEARN_MORE": "Learn more about inboxes",
|
||||||
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
|
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
|
||||||
"CLICK_TO_RECONNECT": "Click here to reconnect.",
|
"CLICK_TO_RECONNECT": "Click here to reconnect.",
|
||||||
|
"WHATSAPP_REGISTRATION_INCOMPLETE": "Your WhatsApp Business registration isn’t complete. Please check your display name status in Meta Business Manager before reconnecting.",
|
||||||
|
"COMPLETE_REGISTRATION": "Complete Registration",
|
||||||
"LIST": {
|
"LIST": {
|
||||||
"404": "K tomuto účtu nejsou připojeny žádné doručené schránky."
|
"404": "K tomuto účtu nejsou připojeny žádné doručené schránky."
|
||||||
},
|
},
|
||||||
@@ -605,8 +607,62 @@
|
|||||||
"BUSINESS_HOURS": "Pracovní doba",
|
"BUSINESS_HOURS": "Pracovní doba",
|
||||||
"WIDGET_BUILDER": "Widget Builder",
|
"WIDGET_BUILDER": "Widget Builder",
|
||||||
"BOT_CONFIGURATION": "Bot Configuration",
|
"BOT_CONFIGURATION": "Bot Configuration",
|
||||||
|
"ACCOUNT_HEALTH": "Account Health",
|
||||||
"CSAT": "CSAT"
|
"CSAT": "CSAT"
|
||||||
},
|
},
|
||||||
|
"ACCOUNT_HEALTH": {
|
||||||
|
"TITLE": "Manage your WhatsApp account",
|
||||||
|
"DESCRIPTION": "Review your WhatsApp account status, messaging limits, and quality. Update settings or resolve issues if needed",
|
||||||
|
"GO_TO_SETTINGS": "Go to Meta Business Manager",
|
||||||
|
"NO_DATA": "Health data is not available",
|
||||||
|
"FIELDS": {
|
||||||
|
"DISPLAY_PHONE_NUMBER": {
|
||||||
|
"LABEL": "Display phone number",
|
||||||
|
"TOOLTIP": "Phone number displayed to customers"
|
||||||
|
},
|
||||||
|
"VERIFIED_NAME": {
|
||||||
|
"LABEL": "Business name",
|
||||||
|
"TOOLTIP": "Business name verified by WhatsApp"
|
||||||
|
},
|
||||||
|
"DISPLAY_NAME_STATUS": {
|
||||||
|
"LABEL": "Display name status",
|
||||||
|
"TOOLTIP": "Status of your business name verification"
|
||||||
|
},
|
||||||
|
"QUALITY_RATING": {
|
||||||
|
"LABEL": "Quality rating",
|
||||||
|
"TOOLTIP": "WhatsApp quality rating for your account"
|
||||||
|
},
|
||||||
|
"MESSAGING_LIMIT_TIER": {
|
||||||
|
"LABEL": "Messaging limit tier",
|
||||||
|
"TOOLTIP": "Daily messaging limit for your account"
|
||||||
|
},
|
||||||
|
"ACCOUNT_MODE": {
|
||||||
|
"LABEL": "Account mode",
|
||||||
|
"TOOLTIP": "Current operating mode of your WhatsApp account"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"VALUES": {
|
||||||
|
"TIERS": {
|
||||||
|
"TIER_250": "250 customers per 24h",
|
||||||
|
"TIER_1000": "1K customers per 24h",
|
||||||
|
"TIER_1K": "1K customers per 24h",
|
||||||
|
"TIER_10K": "10K customers per 24h",
|
||||||
|
"TIER_100K": "100K customers per 24h",
|
||||||
|
"TIER_UNLIMITED": "Unlimited customers per 24h"
|
||||||
|
},
|
||||||
|
"STATUSES": {
|
||||||
|
"APPROVED": "Approved",
|
||||||
|
"PENDING_REVIEW": "Pending Review",
|
||||||
|
"AVAILABLE_WITHOUT_REVIEW": "Available Without Review",
|
||||||
|
"REJECTED": "Rejected",
|
||||||
|
"DECLINED": "Declined"
|
||||||
|
},
|
||||||
|
"MODES": {
|
||||||
|
"SANDBOX": "Sandbox",
|
||||||
|
"LIVE": "Live"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"SETTINGS": "Nastavení",
|
"SETTINGS": "Nastavení",
|
||||||
"FEATURES": {
|
"FEATURES": {
|
||||||
"LABEL": "Funkce",
|
"LABEL": "Funkce",
|
||||||
@@ -618,6 +674,11 @@
|
|||||||
"SETTINGS_POPUP": {
|
"SETTINGS_POPUP": {
|
||||||
"MESSENGER_HEADING": "Messenger skript",
|
"MESSENGER_HEADING": "Messenger skript",
|
||||||
"MESSENGER_SUB_HEAD": "Umístěte toto tlačítko dovnitř vašeho tělesného štítku",
|
"MESSENGER_SUB_HEAD": "Umístěte toto tlačítko dovnitř vašeho tělesného štítku",
|
||||||
|
"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.",
|
||||||
|
"PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)"
|
||||||
|
},
|
||||||
"INBOX_AGENTS": "Agenti",
|
"INBOX_AGENTS": "Agenti",
|
||||||
"INBOX_AGENTS_SUB_TEXT": "Přidat nebo odebrat agenty z této složky doručené pošty",
|
"INBOX_AGENTS_SUB_TEXT": "Přidat nebo odebrat agenty z této složky doručené pošty",
|
||||||
"AGENT_ASSIGNMENT": "Conversation Assignment",
|
"AGENT_ASSIGNMENT": "Conversation Assignment",
|
||||||
|
|||||||
@@ -22,6 +22,20 @@
|
|||||||
},
|
},
|
||||||
"FORGOT_PASSWORD": "Zapomněli jste heslo?",
|
"FORGOT_PASSWORD": "Zapomněli jste heslo?",
|
||||||
"CREATE_NEW_ACCOUNT": "Vytvořit nový účet",
|
"CREATE_NEW_ACCOUNT": "Vytvořit nový účet",
|
||||||
"SUBMIT": "Přihlásit se"
|
"SUBMIT": "Přihlásit se",
|
||||||
|
"SAML": {
|
||||||
|
"LABEL": "Login via SSO",
|
||||||
|
"TITLE": "Initiate Single Sign-on (SSO)",
|
||||||
|
"SUBTITLE": "Enter your work email to access your organization",
|
||||||
|
"BACK_TO_LOGIN": "Login via Password",
|
||||||
|
"WORK_EMAIL": {
|
||||||
|
"LABEL": "Work Email",
|
||||||
|
"PLACEHOLDER": "Enter your work email"
|
||||||
|
},
|
||||||
|
"SUBMIT": "Continue with SSO",
|
||||||
|
"API": {
|
||||||
|
"ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,15 +27,20 @@
|
|||||||
"LABEL": "Heslo",
|
"LABEL": "Heslo",
|
||||||
"PLACEHOLDER": "Heslo",
|
"PLACEHOLDER": "Heslo",
|
||||||
"ERROR": "Heslo je příliš krátké",
|
"ERROR": "Heslo je příliš krátké",
|
||||||
"IS_INVALID_PASSWORD": "Heslo by mělo obsahovat alespoň jedno velké písmeno, jedno malé písmeno, jedno číslo a jeden speciální znak"
|
"IS_INVALID_PASSWORD": "Heslo by mělo obsahovat alespoň jedno velké písmeno, jedno malé písmeno, jedno číslo a jeden speciální znak",
|
||||||
|
"REQUIREMENTS_LENGTH": "At least 6 characters long",
|
||||||
|
"REQUIREMENTS_UPPERCASE": "At least one uppercase letter",
|
||||||
|
"REQUIREMENTS_LOWERCASE": "At least one lowercase letter",
|
||||||
|
"REQUIREMENTS_NUMBER": "At least one number",
|
||||||
|
"REQUIREMENTS_SPECIAL": "At least one special character"
|
||||||
},
|
},
|
||||||
"CONFIRM_PASSWORD": {
|
"CONFIRM_PASSWORD": {
|
||||||
"LABEL": "Potvrzení hesla",
|
"LABEL": "Potvrzení hesla",
|
||||||
"PLACEHOLDER": "Potvrzení hesla",
|
"PLACEHOLDER": "Potvrzení hesla",
|
||||||
"ERROR": "Heslo se neshoduje"
|
"ERROR": "Hesla se neshodují."
|
||||||
},
|
},
|
||||||
"API": {
|
"API": {
|
||||||
"SUCCESS_MESSAGE": "Registrace byla úspěšná",
|
"SUCCESS_MESSAGE": "Registration Successful",
|
||||||
"ERROR_MESSAGE": "Nelze se připojit k Woot serveru, opakujte akci později"
|
"ERROR_MESSAGE": "Nelze se připojit k Woot serveru, opakujte akci později"
|
||||||
},
|
},
|
||||||
"SUBMIT": "Create account",
|
"SUBMIT": "Create account",
|
||||||
|
|||||||
@@ -554,10 +554,12 @@
|
|||||||
"WROTE": "wrote",
|
"WROTE": "wrote",
|
||||||
"YOU": "Dig",
|
"YOU": "Dig",
|
||||||
"SAVE": "Save note",
|
"SAVE": "Save note",
|
||||||
|
"ADD_NOTE": "Add contact note",
|
||||||
"EXPAND": "Expand",
|
"EXPAND": "Expand",
|
||||||
"COLLAPSE": "Collapse",
|
"COLLAPSE": "Collapse",
|
||||||
"NO_NOTES": "No notes, you can add notes from the contact details page.",
|
"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."
|
"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."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"EMPTY_STATE": {
|
"EMPTY_STATE": {
|
||||||
|
|||||||
@@ -227,6 +227,13 @@
|
|||||||
"YES": "Send",
|
"YES": "Send",
|
||||||
"CANCEL": "Annuller"
|
"CANCEL": "Annuller"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"VISIBLE_TO_AGENTS": "Privat Note: Kun synlig for dig og dit team",
|
"VISIBLE_TO_AGENTS": "Privat Note: Kun synlig for dig og dit team",
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
"PLACEHOLDER": "Søg",
|
"PLACEHOLDER": "Søg",
|
||||||
"EMPTY_STATE": "Ingen resultater fundet"
|
"EMPTY_STATE": "Ingen resultater fundet"
|
||||||
},
|
},
|
||||||
"CLOSE": "Luk"
|
"CLOSE": "Luk",
|
||||||
|
"BETA": "Beta",
|
||||||
|
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
"LEARN_MORE": "Learn more about inboxes",
|
"LEARN_MORE": "Learn more about inboxes",
|
||||||
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
|
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
|
||||||
"CLICK_TO_RECONNECT": "Click here to reconnect.",
|
"CLICK_TO_RECONNECT": "Click here to reconnect.",
|
||||||
|
"WHATSAPP_REGISTRATION_INCOMPLETE": "Your WhatsApp Business registration isn’t complete. Please check your display name status in Meta Business Manager before reconnecting.",
|
||||||
|
"COMPLETE_REGISTRATION": "Complete Registration",
|
||||||
"LIST": {
|
"LIST": {
|
||||||
"404": "Der er ingen indbakker tilknyttet denne konto."
|
"404": "Der er ingen indbakker tilknyttet denne konto."
|
||||||
},
|
},
|
||||||
@@ -605,8 +607,62 @@
|
|||||||
"BUSINESS_HOURS": "Forretningstider",
|
"BUSINESS_HOURS": "Forretningstider",
|
||||||
"WIDGET_BUILDER": "Widget Builder",
|
"WIDGET_BUILDER": "Widget Builder",
|
||||||
"BOT_CONFIGURATION": "Bot konfiguration",
|
"BOT_CONFIGURATION": "Bot konfiguration",
|
||||||
|
"ACCOUNT_HEALTH": "Account Health",
|
||||||
"CSAT": "CSAT"
|
"CSAT": "CSAT"
|
||||||
},
|
},
|
||||||
|
"ACCOUNT_HEALTH": {
|
||||||
|
"TITLE": "Manage your WhatsApp account",
|
||||||
|
"DESCRIPTION": "Review your WhatsApp account status, messaging limits, and quality. Update settings or resolve issues if needed",
|
||||||
|
"GO_TO_SETTINGS": "Go to Meta Business Manager",
|
||||||
|
"NO_DATA": "Health data is not available",
|
||||||
|
"FIELDS": {
|
||||||
|
"DISPLAY_PHONE_NUMBER": {
|
||||||
|
"LABEL": "Display phone number",
|
||||||
|
"TOOLTIP": "Phone number displayed to customers"
|
||||||
|
},
|
||||||
|
"VERIFIED_NAME": {
|
||||||
|
"LABEL": "Business name",
|
||||||
|
"TOOLTIP": "Business name verified by WhatsApp"
|
||||||
|
},
|
||||||
|
"DISPLAY_NAME_STATUS": {
|
||||||
|
"LABEL": "Display name status",
|
||||||
|
"TOOLTIP": "Status of your business name verification"
|
||||||
|
},
|
||||||
|
"QUALITY_RATING": {
|
||||||
|
"LABEL": "Quality rating",
|
||||||
|
"TOOLTIP": "WhatsApp quality rating for your account"
|
||||||
|
},
|
||||||
|
"MESSAGING_LIMIT_TIER": {
|
||||||
|
"LABEL": "Messaging limit tier",
|
||||||
|
"TOOLTIP": "Daily messaging limit for your account"
|
||||||
|
},
|
||||||
|
"ACCOUNT_MODE": {
|
||||||
|
"LABEL": "Account mode",
|
||||||
|
"TOOLTIP": "Current operating mode of your WhatsApp account"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"VALUES": {
|
||||||
|
"TIERS": {
|
||||||
|
"TIER_250": "250 customers per 24h",
|
||||||
|
"TIER_1000": "1K customers per 24h",
|
||||||
|
"TIER_1K": "1K customers per 24h",
|
||||||
|
"TIER_10K": "10K customers per 24h",
|
||||||
|
"TIER_100K": "100K customers per 24h",
|
||||||
|
"TIER_UNLIMITED": "Unlimited customers per 24h"
|
||||||
|
},
|
||||||
|
"STATUSES": {
|
||||||
|
"APPROVED": "Approved",
|
||||||
|
"PENDING_REVIEW": "Pending Review",
|
||||||
|
"AVAILABLE_WITHOUT_REVIEW": "Available Without Review",
|
||||||
|
"REJECTED": "Rejected",
|
||||||
|
"DECLINED": "Declined"
|
||||||
|
},
|
||||||
|
"MODES": {
|
||||||
|
"SANDBOX": "Sandbox",
|
||||||
|
"LIVE": "Levende"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"SETTINGS": "Indstillinger",
|
"SETTINGS": "Indstillinger",
|
||||||
"FEATURES": {
|
"FEATURES": {
|
||||||
"LABEL": "Funktioner",
|
"LABEL": "Funktioner",
|
||||||
@@ -618,6 +674,11 @@
|
|||||||
"SETTINGS_POPUP": {
|
"SETTINGS_POPUP": {
|
||||||
"MESSENGER_HEADING": "Messenger- Script",
|
"MESSENGER_HEADING": "Messenger- Script",
|
||||||
"MESSENGER_SUB_HEAD": "Placer denne knap inde i din body tag",
|
"MESSENGER_SUB_HEAD": "Placer denne knap inde i din body tag",
|
||||||
|
"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.",
|
||||||
|
"PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)"
|
||||||
|
},
|
||||||
"INBOX_AGENTS": "Agenter",
|
"INBOX_AGENTS": "Agenter",
|
||||||
"INBOX_AGENTS_SUB_TEXT": "Tilføj eller fjern agenter fra denne indbakke",
|
"INBOX_AGENTS_SUB_TEXT": "Tilføj eller fjern agenter fra denne indbakke",
|
||||||
"AGENT_ASSIGNMENT": "Samtale Tildeling",
|
"AGENT_ASSIGNMENT": "Samtale Tildeling",
|
||||||
|
|||||||
@@ -22,6 +22,20 @@
|
|||||||
},
|
},
|
||||||
"FORGOT_PASSWORD": "Glemt din adgangskode?",
|
"FORGOT_PASSWORD": "Glemt din adgangskode?",
|
||||||
"CREATE_NEW_ACCOUNT": "Opret ny konto",
|
"CREATE_NEW_ACCOUNT": "Opret ny konto",
|
||||||
"SUBMIT": "Log Ind"
|
"SUBMIT": "Log Ind",
|
||||||
|
"SAML": {
|
||||||
|
"LABEL": "Login via SSO",
|
||||||
|
"TITLE": "Initiate Single Sign-on (SSO)",
|
||||||
|
"SUBTITLE": "Enter your work email to access your organization",
|
||||||
|
"BACK_TO_LOGIN": "Login via Password",
|
||||||
|
"WORK_EMAIL": {
|
||||||
|
"LABEL": "Work Email",
|
||||||
|
"PLACEHOLDER": "Enter your work email"
|
||||||
|
},
|
||||||
|
"SUBMIT": "Continue with SSO",
|
||||||
|
"API": {
|
||||||
|
"ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,15 +27,20 @@
|
|||||||
"LABEL": "Adgangskode",
|
"LABEL": "Adgangskode",
|
||||||
"PLACEHOLDER": "Adgangskode",
|
"PLACEHOLDER": "Adgangskode",
|
||||||
"ERROR": "Adgangskoden er for kort",
|
"ERROR": "Adgangskoden er for kort",
|
||||||
"IS_INVALID_PASSWORD": "Adgangskoden skal indeholde mindst 1 stort bogstav, 1 lille bogstav, 1 nummer og 1 specialtegn"
|
"IS_INVALID_PASSWORD": "Adgangskoden skal indeholde mindst 1 stort bogstav, 1 lille bogstav, 1 nummer og 1 specialtegn",
|
||||||
|
"REQUIREMENTS_LENGTH": "At least 6 characters long",
|
||||||
|
"REQUIREMENTS_UPPERCASE": "At least one uppercase letter",
|
||||||
|
"REQUIREMENTS_LOWERCASE": "At least one lowercase letter",
|
||||||
|
"REQUIREMENTS_NUMBER": "At least one number",
|
||||||
|
"REQUIREMENTS_SPECIAL": "At least one special character"
|
||||||
},
|
},
|
||||||
"CONFIRM_PASSWORD": {
|
"CONFIRM_PASSWORD": {
|
||||||
"LABEL": "Bekræft Adgangskode",
|
"LABEL": "Bekræft Adgangskode",
|
||||||
"PLACEHOLDER": "Bekræft Adgangskode",
|
"PLACEHOLDER": "Bekræft Adgangskode",
|
||||||
"ERROR": "Adgangskode stemmer ikke overens"
|
"ERROR": "Adgangskoder stemmer ikke overens."
|
||||||
},
|
},
|
||||||
"API": {
|
"API": {
|
||||||
"SUCCESS_MESSAGE": "Registrering Succesfuld",
|
"SUCCESS_MESSAGE": "Registration Successful",
|
||||||
"ERROR_MESSAGE": "Kunne ikke oprette forbindelse til Woot Server, Prøv igen senere"
|
"ERROR_MESSAGE": "Kunne ikke oprette forbindelse til Woot Server, Prøv igen senere"
|
||||||
},
|
},
|
||||||
"SUBMIT": "Opret en konto",
|
"SUBMIT": "Opret en konto",
|
||||||
|
|||||||
@@ -554,10 +554,12 @@
|
|||||||
"WROTE": "schrieb",
|
"WROTE": "schrieb",
|
||||||
"YOU": "Sie",
|
"YOU": "Sie",
|
||||||
"SAVE": "Notiz speichern",
|
"SAVE": "Notiz speichern",
|
||||||
|
"ADD_NOTE": "Add contact note",
|
||||||
"EXPAND": "Erweitern",
|
"EXPAND": "Erweitern",
|
||||||
"COLLAPSE": "Einklappen",
|
"COLLAPSE": "Einklappen",
|
||||||
"NO_NOTES": "Keine Notizen, Sie können Notizen auf der Kontakt-Detailseite hinzufügen.",
|
"NO_NOTES": "Keine Notizen, Sie können Notizen auf der Kontakt-Detailseite hinzufügen.",
|
||||||
"EMPTY_STATE": "Es gibt keine Notizen zu diesem Kontakt. Sie können eine Notiz hinzufügen, indem Sie diese in das obige Feld eingeben."
|
"EMPTY_STATE": "Es gibt keine Notizen zu diesem Kontakt. Sie können eine Notiz hinzufügen, indem Sie diese in das obige Feld eingeben.",
|
||||||
|
"CONVERSATION_EMPTY_STATE": "There are no notes yet. Use the Add note button to create one."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"EMPTY_STATE": {
|
"EMPTY_STATE": {
|
||||||
|
|||||||
@@ -227,6 +227,13 @@
|
|||||||
"YES": "Senden",
|
"YES": "Senden",
|
||||||
"CANCEL": "Abbrechen"
|
"CANCEL": "Abbrechen"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"VISIBLE_TO_AGENTS": "Privater Hinweis: Nur für Sie und Ihr Team sichtbar",
|
"VISIBLE_TO_AGENTS": "Privater Hinweis: Nur für Sie und Ihr Team sichtbar",
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
"PLACEHOLDER": "Suchen",
|
"PLACEHOLDER": "Suchen",
|
||||||
"EMPTY_STATE": "Keine Ergebnisse gefunden"
|
"EMPTY_STATE": "Keine Ergebnisse gefunden"
|
||||||
},
|
},
|
||||||
"CLOSE": "Schließen"
|
"CLOSE": "Schließen",
|
||||||
|
"BETA": "Beta",
|
||||||
|
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
"LEARN_MORE": "Mehr über Posteingänge erfahren",
|
"LEARN_MORE": "Mehr über Posteingänge erfahren",
|
||||||
"RECONNECTION_REQUIRED": "Ihr Posteingang ist nicht verbunden. Sie erhalten keine neuen Nachrichten, bis Sie ihn erneut autorisieren.",
|
"RECONNECTION_REQUIRED": "Ihr Posteingang ist nicht verbunden. Sie erhalten keine neuen Nachrichten, bis Sie ihn erneut autorisieren.",
|
||||||
"CLICK_TO_RECONNECT": "Klicken Sie hier, um die Verbindung wiederherzustellen.",
|
"CLICK_TO_RECONNECT": "Klicken Sie hier, um die Verbindung wiederherzustellen.",
|
||||||
|
"WHATSAPP_REGISTRATION_INCOMPLETE": "Your WhatsApp Business registration isn’t complete. Please check your display name status in Meta Business Manager before reconnecting.",
|
||||||
|
"COMPLETE_REGISTRATION": "Complete Registration",
|
||||||
"LIST": {
|
"LIST": {
|
||||||
"404": "Diesem Konto sind keine Posteingänge zugeordnet."
|
"404": "Diesem Konto sind keine Posteingänge zugeordnet."
|
||||||
},
|
},
|
||||||
@@ -605,8 +607,62 @@
|
|||||||
"BUSINESS_HOURS": "Öffnungszeiten",
|
"BUSINESS_HOURS": "Öffnungszeiten",
|
||||||
"WIDGET_BUILDER": "Widget-Generator",
|
"WIDGET_BUILDER": "Widget-Generator",
|
||||||
"BOT_CONFIGURATION": "Bot-Konfiguration",
|
"BOT_CONFIGURATION": "Bot-Konfiguration",
|
||||||
|
"ACCOUNT_HEALTH": "Account Health",
|
||||||
"CSAT": "CSAT"
|
"CSAT": "CSAT"
|
||||||
},
|
},
|
||||||
|
"ACCOUNT_HEALTH": {
|
||||||
|
"TITLE": "Manage your WhatsApp account",
|
||||||
|
"DESCRIPTION": "Review your WhatsApp account status, messaging limits, and quality. Update settings or resolve issues if needed",
|
||||||
|
"GO_TO_SETTINGS": "Go to Meta Business Manager",
|
||||||
|
"NO_DATA": "Health data is not available",
|
||||||
|
"FIELDS": {
|
||||||
|
"DISPLAY_PHONE_NUMBER": {
|
||||||
|
"LABEL": "Display phone number",
|
||||||
|
"TOOLTIP": "Phone number displayed to customers"
|
||||||
|
},
|
||||||
|
"VERIFIED_NAME": {
|
||||||
|
"LABEL": "Business name",
|
||||||
|
"TOOLTIP": "Business name verified by WhatsApp"
|
||||||
|
},
|
||||||
|
"DISPLAY_NAME_STATUS": {
|
||||||
|
"LABEL": "Display name status",
|
||||||
|
"TOOLTIP": "Status of your business name verification"
|
||||||
|
},
|
||||||
|
"QUALITY_RATING": {
|
||||||
|
"LABEL": "Quality rating",
|
||||||
|
"TOOLTIP": "WhatsApp quality rating for your account"
|
||||||
|
},
|
||||||
|
"MESSAGING_LIMIT_TIER": {
|
||||||
|
"LABEL": "Messaging limit tier",
|
||||||
|
"TOOLTIP": "Daily messaging limit for your account"
|
||||||
|
},
|
||||||
|
"ACCOUNT_MODE": {
|
||||||
|
"LABEL": "Account mode",
|
||||||
|
"TOOLTIP": "Current operating mode of your WhatsApp account"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"VALUES": {
|
||||||
|
"TIERS": {
|
||||||
|
"TIER_250": "250 customers per 24h",
|
||||||
|
"TIER_1000": "1K customers per 24h",
|
||||||
|
"TIER_1K": "1K customers per 24h",
|
||||||
|
"TIER_10K": "10K customers per 24h",
|
||||||
|
"TIER_100K": "100K customers per 24h",
|
||||||
|
"TIER_UNLIMITED": "Unlimited customers per 24h"
|
||||||
|
},
|
||||||
|
"STATUSES": {
|
||||||
|
"APPROVED": "Approved",
|
||||||
|
"PENDING_REVIEW": "Pending Review",
|
||||||
|
"AVAILABLE_WITHOUT_REVIEW": "Available Without Review",
|
||||||
|
"REJECTED": "Rejected",
|
||||||
|
"DECLINED": "Declined"
|
||||||
|
},
|
||||||
|
"MODES": {
|
||||||
|
"SANDBOX": "Sandbox",
|
||||||
|
"LIVE": "Live"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"SETTINGS": "Einstellungen",
|
"SETTINGS": "Einstellungen",
|
||||||
"FEATURES": {
|
"FEATURES": {
|
||||||
"LABEL": "Funktionen",
|
"LABEL": "Funktionen",
|
||||||
@@ -618,6 +674,11 @@
|
|||||||
"SETTINGS_POPUP": {
|
"SETTINGS_POPUP": {
|
||||||
"MESSENGER_HEADING": "Messenger-Skript",
|
"MESSENGER_HEADING": "Messenger-Skript",
|
||||||
"MESSENGER_SUB_HEAD": "Platzieren Sie diese Schaltfläche in Ihrem Body-Tag",
|
"MESSENGER_SUB_HEAD": "Platzieren Sie diese Schaltfläche in Ihrem Body-Tag",
|
||||||
|
"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.",
|
||||||
|
"PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)"
|
||||||
|
},
|
||||||
"INBOX_AGENTS": "Agenten",
|
"INBOX_AGENTS": "Agenten",
|
||||||
"INBOX_AGENTS_SUB_TEXT": "Hinzufügen oder Entfernen von Agenten zu diesem Posteingang",
|
"INBOX_AGENTS_SUB_TEXT": "Hinzufügen oder Entfernen von Agenten zu diesem Posteingang",
|
||||||
"AGENT_ASSIGNMENT": "Konversationssauftrag",
|
"AGENT_ASSIGNMENT": "Konversationssauftrag",
|
||||||
|
|||||||
@@ -22,6 +22,20 @@
|
|||||||
},
|
},
|
||||||
"FORGOT_PASSWORD": "Haben Sie Ihr Passwort vergessen?",
|
"FORGOT_PASSWORD": "Haben Sie Ihr Passwort vergessen?",
|
||||||
"CREATE_NEW_ACCOUNT": "Neuen Account erstellen",
|
"CREATE_NEW_ACCOUNT": "Neuen Account erstellen",
|
||||||
"SUBMIT": "Einloggen"
|
"SUBMIT": "Einloggen",
|
||||||
|
"SAML": {
|
||||||
|
"LABEL": "Login via SSO",
|
||||||
|
"TITLE": "Initiate Single Sign-on (SSO)",
|
||||||
|
"SUBTITLE": "Enter your work email to access your organization",
|
||||||
|
"BACK_TO_LOGIN": "Login via Password",
|
||||||
|
"WORK_EMAIL": {
|
||||||
|
"LABEL": "Work Email",
|
||||||
|
"PLACEHOLDER": "Enter your work email"
|
||||||
|
},
|
||||||
|
"SUBMIT": "Continue with SSO",
|
||||||
|
"API": {
|
||||||
|
"ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,15 +27,20 @@
|
|||||||
"LABEL": "Passwort",
|
"LABEL": "Passwort",
|
||||||
"PLACEHOLDER": "Passwort",
|
"PLACEHOLDER": "Passwort",
|
||||||
"ERROR": "Das Passwort ist zu kurz",
|
"ERROR": "Das Passwort ist zu kurz",
|
||||||
"IS_INVALID_PASSWORD": "Das Passwort sollte mindestens 1 Großbuchstaben, 1 Kleinbuchstaben, 1 Ziffer und 1 Sonderzeichen enthalten"
|
"IS_INVALID_PASSWORD": "Das Passwort sollte mindestens 1 Großbuchstaben, 1 Kleinbuchstaben, 1 Ziffer und 1 Sonderzeichen enthalten",
|
||||||
|
"REQUIREMENTS_LENGTH": "At least 6 characters long",
|
||||||
|
"REQUIREMENTS_UPPERCASE": "At least one uppercase letter",
|
||||||
|
"REQUIREMENTS_LOWERCASE": "At least one lowercase letter",
|
||||||
|
"REQUIREMENTS_NUMBER": "At least one number",
|
||||||
|
"REQUIREMENTS_SPECIAL": "At least one special character"
|
||||||
},
|
},
|
||||||
"CONFIRM_PASSWORD": {
|
"CONFIRM_PASSWORD": {
|
||||||
"LABEL": "Bestätige das Passwort",
|
"LABEL": "Bestätige das Passwort",
|
||||||
"PLACEHOLDER": "Bestätige das Passwort",
|
"PLACEHOLDER": "Bestätige das Passwort",
|
||||||
"ERROR": "Passwort stimmt nicht überein"
|
"ERROR": "Passwörter stimmen nicht überein."
|
||||||
},
|
},
|
||||||
"API": {
|
"API": {
|
||||||
"SUCCESS_MESSAGE": "Registrierung erfolgreich",
|
"SUCCESS_MESSAGE": "Registration Successful",
|
||||||
"ERROR_MESSAGE": "Es konnte keine Verbindung zum Woot Server hergestellt werden. Bitte versuchen Sie es später erneut"
|
"ERROR_MESSAGE": "Es konnte keine Verbindung zum Woot Server hergestellt werden. Bitte versuchen Sie es später erneut"
|
||||||
},
|
},
|
||||||
"SUBMIT": "Konto erstellen",
|
"SUBMIT": "Konto erstellen",
|
||||||
|
|||||||
@@ -554,10 +554,12 @@
|
|||||||
"WROTE": "wrote",
|
"WROTE": "wrote",
|
||||||
"YOU": "You",
|
"YOU": "You",
|
||||||
"SAVE": "Save note",
|
"SAVE": "Save note",
|
||||||
|
"ADD_NOTE": "Add contact note",
|
||||||
"EXPAND": "Expand",
|
"EXPAND": "Expand",
|
||||||
"COLLAPSE": "Collapse",
|
"COLLAPSE": "Collapse",
|
||||||
"NO_NOTES": "No notes, you can add notes from the contact details page.",
|
"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."
|
"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."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"EMPTY_STATE": {
|
"EMPTY_STATE": {
|
||||||
|
|||||||
@@ -227,6 +227,13 @@
|
|||||||
"YES": "Αποστολή",
|
"YES": "Αποστολή",
|
||||||
"CANCEL": "Άκυρο"
|
"CANCEL": "Άκυρο"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"VISIBLE_TO_AGENTS": "Ιδιωτική Σημείωση: Ορατή μόνο σε σας και την ομάδα σας",
|
"VISIBLE_TO_AGENTS": "Ιδιωτική Σημείωση: Ορατή μόνο σε σας και την ομάδα σας",
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
"PLACEHOLDER": "Αναζήτηση",
|
"PLACEHOLDER": "Αναζήτηση",
|
||||||
"EMPTY_STATE": "Δεν βρέθηκαν αποτελέσματα"
|
"EMPTY_STATE": "Δεν βρέθηκαν αποτελέσματα"
|
||||||
},
|
},
|
||||||
"CLOSE": "Κλείσιμο"
|
"CLOSE": "Κλείσιμο",
|
||||||
|
"BETA": "Beta",
|
||||||
|
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
"LEARN_MORE": "Learn more about inboxes",
|
"LEARN_MORE": "Learn more about inboxes",
|
||||||
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
|
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
|
||||||
"CLICK_TO_RECONNECT": "Click here to reconnect.",
|
"CLICK_TO_RECONNECT": "Click here to reconnect.",
|
||||||
|
"WHATSAPP_REGISTRATION_INCOMPLETE": "Your WhatsApp Business registration isn’t complete. Please check your display name status in Meta Business Manager before reconnecting.",
|
||||||
|
"COMPLETE_REGISTRATION": "Complete Registration",
|
||||||
"LIST": {
|
"LIST": {
|
||||||
"404": "Δεν υπάρχουν κιβώτια εισερχομένων σε αυτόν τον λογαριασμό."
|
"404": "Δεν υπάρχουν κιβώτια εισερχομένων σε αυτόν τον λογαριασμό."
|
||||||
},
|
},
|
||||||
@@ -605,8 +607,62 @@
|
|||||||
"BUSINESS_HOURS": "Ώρες Εργασίας",
|
"BUSINESS_HOURS": "Ώρες Εργασίας",
|
||||||
"WIDGET_BUILDER": "Δημιουργός Widget",
|
"WIDGET_BUILDER": "Δημιουργός Widget",
|
||||||
"BOT_CONFIGURATION": "Ρυθμίσεις Bot",
|
"BOT_CONFIGURATION": "Ρυθμίσεις Bot",
|
||||||
|
"ACCOUNT_HEALTH": "Account Health",
|
||||||
"CSAT": "CSAT"
|
"CSAT": "CSAT"
|
||||||
},
|
},
|
||||||
|
"ACCOUNT_HEALTH": {
|
||||||
|
"TITLE": "Manage your WhatsApp account",
|
||||||
|
"DESCRIPTION": "Review your WhatsApp account status, messaging limits, and quality. Update settings or resolve issues if needed",
|
||||||
|
"GO_TO_SETTINGS": "Go to Meta Business Manager",
|
||||||
|
"NO_DATA": "Health data is not available",
|
||||||
|
"FIELDS": {
|
||||||
|
"DISPLAY_PHONE_NUMBER": {
|
||||||
|
"LABEL": "Display phone number",
|
||||||
|
"TOOLTIP": "Phone number displayed to customers"
|
||||||
|
},
|
||||||
|
"VERIFIED_NAME": {
|
||||||
|
"LABEL": "Business name",
|
||||||
|
"TOOLTIP": "Business name verified by WhatsApp"
|
||||||
|
},
|
||||||
|
"DISPLAY_NAME_STATUS": {
|
||||||
|
"LABEL": "Display name status",
|
||||||
|
"TOOLTIP": "Status of your business name verification"
|
||||||
|
},
|
||||||
|
"QUALITY_RATING": {
|
||||||
|
"LABEL": "Quality rating",
|
||||||
|
"TOOLTIP": "WhatsApp quality rating for your account"
|
||||||
|
},
|
||||||
|
"MESSAGING_LIMIT_TIER": {
|
||||||
|
"LABEL": "Messaging limit tier",
|
||||||
|
"TOOLTIP": "Daily messaging limit for your account"
|
||||||
|
},
|
||||||
|
"ACCOUNT_MODE": {
|
||||||
|
"LABEL": "Account mode",
|
||||||
|
"TOOLTIP": "Current operating mode of your WhatsApp account"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"VALUES": {
|
||||||
|
"TIERS": {
|
||||||
|
"TIER_250": "250 customers per 24h",
|
||||||
|
"TIER_1000": "1K customers per 24h",
|
||||||
|
"TIER_1K": "1K customers per 24h",
|
||||||
|
"TIER_10K": "10K customers per 24h",
|
||||||
|
"TIER_100K": "100K customers per 24h",
|
||||||
|
"TIER_UNLIMITED": "Unlimited customers per 24h"
|
||||||
|
},
|
||||||
|
"STATUSES": {
|
||||||
|
"APPROVED": "Approved",
|
||||||
|
"PENDING_REVIEW": "Pending Review",
|
||||||
|
"AVAILABLE_WITHOUT_REVIEW": "Available Without Review",
|
||||||
|
"REJECTED": "Rejected",
|
||||||
|
"DECLINED": "Declined"
|
||||||
|
},
|
||||||
|
"MODES": {
|
||||||
|
"SANDBOX": "Sandbox",
|
||||||
|
"LIVE": "Ζωντανά"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"SETTINGS": "Ρυθμίσεις",
|
"SETTINGS": "Ρυθμίσεις",
|
||||||
"FEATURES": {
|
"FEATURES": {
|
||||||
"LABEL": "Χαρακτηριστικά",
|
"LABEL": "Χαρακτηριστικά",
|
||||||
@@ -618,6 +674,11 @@
|
|||||||
"SETTINGS_POPUP": {
|
"SETTINGS_POPUP": {
|
||||||
"MESSENGER_HEADING": "Κώδικας (Script)",
|
"MESSENGER_HEADING": "Κώδικας (Script)",
|
||||||
"MESSENGER_SUB_HEAD": "Τοποθετήσετε αυτόν τον κώδικα μέσα στο body tag της ιστοσελίδας σας",
|
"MESSENGER_SUB_HEAD": "Τοποθετήσετε αυτόν τον κώδικα μέσα στο body tag της ιστοσελίδας σας",
|
||||||
|
"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.",
|
||||||
|
"PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)"
|
||||||
|
},
|
||||||
"INBOX_AGENTS": "Πράκτορες",
|
"INBOX_AGENTS": "Πράκτορες",
|
||||||
"INBOX_AGENTS_SUB_TEXT": "Προσθέστε ή αφαιρέστε πράκτορες σε αυτό το κιβώτιο",
|
"INBOX_AGENTS_SUB_TEXT": "Προσθέστε ή αφαιρέστε πράκτορες σε αυτό το κιβώτιο",
|
||||||
"AGENT_ASSIGNMENT": "Ανάθεση Συνομιλίας",
|
"AGENT_ASSIGNMENT": "Ανάθεση Συνομιλίας",
|
||||||
|
|||||||
@@ -22,6 +22,20 @@
|
|||||||
},
|
},
|
||||||
"FORGOT_PASSWORD": "Ξεχάσατε τον κωδικό;",
|
"FORGOT_PASSWORD": "Ξεχάσατε τον κωδικό;",
|
||||||
"CREATE_NEW_ACCOUNT": "Δημιουργία νέου Λογαριασμού",
|
"CREATE_NEW_ACCOUNT": "Δημιουργία νέου Λογαριασμού",
|
||||||
"SUBMIT": "Είσοδος"
|
"SUBMIT": "Είσοδος",
|
||||||
|
"SAML": {
|
||||||
|
"LABEL": "Login via SSO",
|
||||||
|
"TITLE": "Initiate Single Sign-on (SSO)",
|
||||||
|
"SUBTITLE": "Enter your work email to access your organization",
|
||||||
|
"BACK_TO_LOGIN": "Login via Password",
|
||||||
|
"WORK_EMAIL": {
|
||||||
|
"LABEL": "Work Email",
|
||||||
|
"PLACEHOLDER": "Enter your work email"
|
||||||
|
},
|
||||||
|
"SUBMIT": "Continue with SSO",
|
||||||
|
"API": {
|
||||||
|
"ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,15 +27,20 @@
|
|||||||
"LABEL": "Κωδικός",
|
"LABEL": "Κωδικός",
|
||||||
"PLACEHOLDER": "Κωδικός",
|
"PLACEHOLDER": "Κωδικός",
|
||||||
"ERROR": "Ο κωδικός είναι πολύ σύντομος",
|
"ERROR": "Ο κωδικός είναι πολύ σύντομος",
|
||||||
"IS_INVALID_PASSWORD": "Ο κωδικός πρόσβασης πρέπει να περιέχει τουλάχιστον 1 κεφαλαίο γράμμα, 1 πεζό γράμμα, 1 αριθμό και 1 ειδικό χαρακτήρα"
|
"IS_INVALID_PASSWORD": "Ο κωδικός πρόσβασης πρέπει να περιέχει τουλάχιστον 1 κεφαλαίο γράμμα, 1 πεζό γράμμα, 1 αριθμό και 1 ειδικό χαρακτήρα",
|
||||||
|
"REQUIREMENTS_LENGTH": "At least 6 characters long",
|
||||||
|
"REQUIREMENTS_UPPERCASE": "At least one uppercase letter",
|
||||||
|
"REQUIREMENTS_LOWERCASE": "At least one lowercase letter",
|
||||||
|
"REQUIREMENTS_NUMBER": "At least one number",
|
||||||
|
"REQUIREMENTS_SPECIAL": "At least one special character"
|
||||||
},
|
},
|
||||||
"CONFIRM_PASSWORD": {
|
"CONFIRM_PASSWORD": {
|
||||||
"LABEL": "Επιβεβαίωση κωδικού",
|
"LABEL": "Επιβεβαίωση κωδικού",
|
||||||
"PLACEHOLDER": "Επιβεβαίωση κωδικού",
|
"PLACEHOLDER": "Επιβεβαίωση κωδικού",
|
||||||
"ERROR": "Οι κωδικοί δεν συμφωνούν"
|
"ERROR": "Οι κωδικοί δεν ταιριάζουν."
|
||||||
},
|
},
|
||||||
"API": {
|
"API": {
|
||||||
"SUCCESS_MESSAGE": "Επιτυχής καταχώρηση",
|
"SUCCESS_MESSAGE": "Registration Successful",
|
||||||
"ERROR_MESSAGE": "Αδυναμία σύνδεσης με τον Woot Server, Παρακαλώ προσπαθήστε αργότερα"
|
"ERROR_MESSAGE": "Αδυναμία σύνδεσης με τον Woot Server, Παρακαλώ προσπαθήστε αργότερα"
|
||||||
},
|
},
|
||||||
"SUBMIT": "Create account",
|
"SUBMIT": "Create account",
|
||||||
|
|||||||
@@ -227,6 +227,13 @@
|
|||||||
"YES": "Send",
|
"YES": "Send",
|
||||||
"CANCEL": "Cancel"
|
"CANCEL": "Cancel"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team",
|
"VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team",
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
"PLACEHOLDER": "Search",
|
"PLACEHOLDER": "Search",
|
||||||
"EMPTY_STATE": "No results found"
|
"EMPTY_STATE": "No results found"
|
||||||
},
|
},
|
||||||
"CLOSE": "Close"
|
"CLOSE": "Close",
|
||||||
|
"BETA": "Beta",
|
||||||
|
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
"LEARN_MORE": "Learn more about inboxes",
|
"LEARN_MORE": "Learn more about inboxes",
|
||||||
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
|
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
|
||||||
"CLICK_TO_RECONNECT": "Click here to reconnect.",
|
"CLICK_TO_RECONNECT": "Click here to reconnect.",
|
||||||
|
"WHATSAPP_REGISTRATION_INCOMPLETE": "Your WhatsApp Business registration isn’t complete. Please check your display name status in Meta Business Manager before reconnecting.",
|
||||||
|
"COMPLETE_REGISTRATION": "Complete Registration",
|
||||||
"LIST": {
|
"LIST": {
|
||||||
"404": "There are no inboxes attached to this account."
|
"404": "There are no inboxes attached to this account."
|
||||||
},
|
},
|
||||||
@@ -605,8 +607,64 @@
|
|||||||
"BUSINESS_HOURS": "Business Hours",
|
"BUSINESS_HOURS": "Business Hours",
|
||||||
"WIDGET_BUILDER": "Widget Builder",
|
"WIDGET_BUILDER": "Widget Builder",
|
||||||
"BOT_CONFIGURATION": "Bot Configuration",
|
"BOT_CONFIGURATION": "Bot Configuration",
|
||||||
|
"ACCOUNT_HEALTH": "Account Health",
|
||||||
"CSAT": "CSAT"
|
"CSAT": "CSAT"
|
||||||
},
|
},
|
||||||
|
"ACCOUNT_HEALTH": {
|
||||||
|
"TITLE": "Manage your WhatsApp account",
|
||||||
|
"DESCRIPTION": "Review your WhatsApp account status, messaging limits, and quality. Update settings or resolve issues if needed",
|
||||||
|
"GO_TO_SETTINGS": "Go to Meta Business Manager",
|
||||||
|
"NO_DATA": "Health data is not available",
|
||||||
|
"FIELDS": {
|
||||||
|
"DISPLAY_PHONE_NUMBER": {
|
||||||
|
"LABEL": "Display phone number",
|
||||||
|
"TOOLTIP": "Phone number displayed to customers"
|
||||||
|
},
|
||||||
|
"VERIFIED_NAME": {
|
||||||
|
"LABEL": "Business name",
|
||||||
|
"TOOLTIP": "Business name verified by WhatsApp"
|
||||||
|
},
|
||||||
|
"DISPLAY_NAME_STATUS": {
|
||||||
|
"LABEL": "Display name status",
|
||||||
|
"TOOLTIP": "Status of your business name verification"
|
||||||
|
},
|
||||||
|
"QUALITY_RATING": {
|
||||||
|
"LABEL": "Quality rating",
|
||||||
|
"TOOLTIP": "WhatsApp quality rating for your account"
|
||||||
|
},
|
||||||
|
"MESSAGING_LIMIT_TIER": {
|
||||||
|
"LABEL": "Messaging limit tier",
|
||||||
|
"TOOLTIP": "Daily messaging limit for your account"
|
||||||
|
},
|
||||||
|
"ACCOUNT_MODE": {
|
||||||
|
"LABEL": "Account mode",
|
||||||
|
"TOOLTIP": "Current operating mode of your WhatsApp account"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"VALUES": {
|
||||||
|
"TIERS": {
|
||||||
|
"TIER_250": "250 customers per 24h",
|
||||||
|
"TIER_1000": "1K customers per 24h",
|
||||||
|
"TIER_1K": "1K customers per 24h",
|
||||||
|
"TIER_10K": "10K customers per 24h",
|
||||||
|
"TIER_100K": "100K customers per 24h",
|
||||||
|
"TIER_UNLIMITED": "Unlimited customers per 24h",
|
||||||
|
"UNKNOWN": "Rating not available"
|
||||||
|
},
|
||||||
|
"STATUSES": {
|
||||||
|
"APPROVED": "Approved",
|
||||||
|
"PENDING_REVIEW": "Pending Review",
|
||||||
|
"AVAILABLE_WITHOUT_REVIEW": "Available Without Review",
|
||||||
|
"REJECTED": "Rejected",
|
||||||
|
"DECLINED": "Declined",
|
||||||
|
"NON_EXISTS": "Non exists"
|
||||||
|
},
|
||||||
|
"MODES": {
|
||||||
|
"SANDBOX": "Sandbox",
|
||||||
|
"LIVE": "Live"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"SETTINGS": "Settings",
|
"SETTINGS": "Settings",
|
||||||
"FEATURES": {
|
"FEATURES": {
|
||||||
"LABEL": "Features",
|
"LABEL": "Features",
|
||||||
@@ -618,6 +676,11 @@
|
|||||||
"SETTINGS_POPUP": {
|
"SETTINGS_POPUP": {
|
||||||
"MESSENGER_HEADING": "Messenger Script",
|
"MESSENGER_HEADING": "Messenger Script",
|
||||||
"MESSENGER_SUB_HEAD": "Place this button inside your body tag",
|
"MESSENGER_SUB_HEAD": "Place this button inside your body tag",
|
||||||
|
"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.",
|
||||||
|
"PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)"
|
||||||
|
},
|
||||||
"INBOX_AGENTS": "Agents",
|
"INBOX_AGENTS": "Agents",
|
||||||
"INBOX_AGENTS_SUB_TEXT": "Add or remove agents from this inbox",
|
"INBOX_AGENTS_SUB_TEXT": "Add or remove agents from this inbox",
|
||||||
"AGENT_ASSIGNMENT": "Conversation Assignment",
|
"AGENT_ASSIGNMENT": "Conversation Assignment",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user