Merge branch 'release/3.3.0'
This commit is contained in:
commit
bda2d1d9a4
21
.env.example
21
.env.example
@ -81,7 +81,7 @@ SMTP_OPENSSL_VERIFY_MODE=peer
|
||||
# Mail Incoming
|
||||
# This is the domain set for the reply emails when conversation continuity is enabled
|
||||
MAILER_INBOUND_EMAIL_DOMAIN=
|
||||
# Set this to appropriate ingress channel with regards to incoming emails
|
||||
# Set this to the appropriate ingress channel with regards to incoming emails
|
||||
# Possible values are :
|
||||
# relay for Exim, Postfix, Qmail
|
||||
# mailgun for Mailgun
|
||||
@ -91,10 +91,20 @@ MAILER_INBOUND_EMAIL_DOMAIN=
|
||||
RAILS_INBOUND_EMAIL_SERVICE=
|
||||
# Use one of the following based on the email ingress service
|
||||
# Ref: https://edgeguides.rubyonrails.org/action_mailbox_basics.html
|
||||
# Set this to a password of your choice and use it in the Inbound webhook
|
||||
RAILS_INBOUND_EMAIL_PASSWORD=
|
||||
|
||||
MAILGUN_INGRESS_SIGNING_KEY=
|
||||
MANDRILL_INGRESS_API_KEY=
|
||||
|
||||
# Creating Your Inbound Webhook Instructions for Postmark and Sendgrid:
|
||||
# Inbound webhook URL format:
|
||||
# https://actionmailbox:[YOUR_RAILS_INBOUND_EMAIL_PASSWORD]@[YOUR_CHATWOOT_DOMAIN.COM]/rails/action_mailbox/[RAILS_INBOUND_EMAIL_SERVICE]/inbound_emails
|
||||
# Note: Replace the values inside the brackets; do not include the brackets themselves.
|
||||
# Example: https://actionmailbox:mYRandomPassword3@chatwoot.example.com/rails/action_mailbox/postmark/inbound_emails
|
||||
# For Postmark
|
||||
# Ensure the 'Include raw email content in JSON payload' checkbox is selected in the inbound webhook section.
|
||||
|
||||
# Storage
|
||||
ACTIVE_STORAGE_SERVICE=local
|
||||
|
||||
@ -176,6 +186,9 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
|
||||
# MICROSOFT CLARITY
|
||||
# MS_CLARITY_TOKEN=xxxxxxxxx
|
||||
|
||||
# GOOGLE_TAG_MANAGER
|
||||
# GOOGLE_TAG = GTM-XXXXXXX
|
||||
|
||||
## Scout
|
||||
## https://scoutapm.com/docs/ruby/configuration
|
||||
# SCOUT_KEY=YOURKEY
|
||||
@ -240,3 +253,9 @@ AZURE_APP_SECRET=
|
||||
|
||||
# Sentiment analysis model file path
|
||||
SENTIMENT_FILE_PATH=
|
||||
|
||||
|
||||
# Housekeeping/Performance related configurations
|
||||
# Set to true if you want to remove stale contact inboxes
|
||||
# contact_inboxes with no conversation older than 90 days will be removed
|
||||
# REMOVE_STALE_CONTACT_INBOX_JOB_STATUS=false
|
||||
|
||||
@ -4,6 +4,7 @@ module.exports = {
|
||||
'prettier',
|
||||
'plugin:vue/recommended',
|
||||
'plugin:storybook/recommended',
|
||||
'plugin:cypress/recommended',
|
||||
],
|
||||
parserOptions: {
|
||||
parser: '@babel/eslint-parser',
|
||||
|
||||
12
Gemfile
12
Gemfile
@ -107,16 +107,16 @@ gem 'google-cloud-translate-v3'
|
||||
gem 'ddtrace', require: false
|
||||
gem 'elastic-apm', require: false
|
||||
gem 'newrelic_rpm', require: false
|
||||
gem 'newrelic-sidekiq-metrics', require: false
|
||||
gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false
|
||||
gem 'scout_apm', require: false
|
||||
gem 'sentry-rails', '>= 5.12.0', require: false
|
||||
gem 'sentry-rails', '>= 5.13.0', require: false
|
||||
gem 'sentry-ruby', require: false
|
||||
gem 'sentry-sidekiq', '>= 5.12.0', require: false
|
||||
gem 'sentry-sidekiq', '>= 5.13.0', require: false
|
||||
|
||||
##-- background job processing --##
|
||||
gem 'sidekiq', '>= 7.1.3'
|
||||
# We want cron jobs
|
||||
gem 'sidekiq-cron', '>= 1.10.1'
|
||||
gem 'sidekiq-cron', '>= 1.11.0'
|
||||
|
||||
##-- Push notification service --##
|
||||
gem 'fcm'
|
||||
@ -159,7 +159,7 @@ gem 'lograge', '~> 0.14.0', require: false
|
||||
# worked with microsoft refresh token
|
||||
gem 'omniauth-oauth2'
|
||||
|
||||
gem 'audited', '~> 5.4', '>= 5.4.0'
|
||||
gem 'audited', '~> 5.4', '>= 5.4.1'
|
||||
|
||||
# need for google auth
|
||||
gem 'omniauth'
|
||||
@ -200,6 +200,8 @@ group :development do
|
||||
# profiling
|
||||
gem 'rack-mini-profiler', '>= 3.1.1', require: false
|
||||
gem 'stackprof'
|
||||
# Should install the associated chrome extension to view query logs
|
||||
gem 'meta_request'
|
||||
end
|
||||
|
||||
group :test do
|
||||
|
||||
58
Gemfile.lock
58
Gemfile.lock
@ -126,9 +126,9 @@ GEM
|
||||
rake (>= 10.4, < 14.0)
|
||||
ast (2.4.2)
|
||||
attr_extras (7.1.0)
|
||||
audited (5.4.0)
|
||||
activerecord (>= 5.0, < 7.2)
|
||||
request_store (~> 1.2)
|
||||
audited (5.4.1)
|
||||
activerecord (>= 5.0, < 7.7)
|
||||
activesupport (>= 5.0, < 7.7)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.760.0)
|
||||
aws-sdk-core (3.171.1)
|
||||
@ -148,6 +148,7 @@ GEM
|
||||
barnes (0.0.9)
|
||||
multi_json (~> 1)
|
||||
statsd-ruby (~> 1.1)
|
||||
base64 (0.1.1)
|
||||
bcrypt (3.1.19)
|
||||
bindex (0.8.1)
|
||||
blingfire (0.1.8)
|
||||
@ -174,7 +175,7 @@ GEM
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
csv-safe (3.2.1)
|
||||
cypress-on-rails (1.13.1)
|
||||
cypress-on-rails (1.16.0)
|
||||
rack
|
||||
database_cleaner (2.0.2)
|
||||
database_cleaner-active_record (>= 2, < 3)
|
||||
@ -261,7 +262,7 @@ GEM
|
||||
rake
|
||||
flag_shih_tzu (0.3.23)
|
||||
foreman (0.87.2)
|
||||
fugit (1.8.1)
|
||||
fugit (1.9.0)
|
||||
et-orbi (~> 1, >= 1.2.7)
|
||||
raabro (~> 1.4)
|
||||
gapic-common (0.18.0)
|
||||
@ -450,13 +451,16 @@ GEM
|
||||
marcel (1.0.2)
|
||||
maxminddb (0.1.22)
|
||||
memoist (0.16.2)
|
||||
meta_request (0.7.4)
|
||||
rack-contrib (>= 1.1, < 3)
|
||||
railties (>= 3.0.0, < 7.1)
|
||||
method_source (1.0.0)
|
||||
mime-types (3.4.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2023.0218.1)
|
||||
mini_magick (4.12.0)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.4)
|
||||
mini_portile2 (2.8.5)
|
||||
minitest (5.20.0)
|
||||
mock_redis (0.36.0)
|
||||
ruby2_keywords
|
||||
@ -478,10 +482,11 @@ GEM
|
||||
net-smtp (0.3.3)
|
||||
net-protocol
|
||||
netrc (0.11.0)
|
||||
newrelic-sidekiq-metrics (1.6.1)
|
||||
newrelic_rpm (~> 8)
|
||||
newrelic-sidekiq-metrics (1.6.2)
|
||||
newrelic_rpm (>= 8.0.0)
|
||||
sidekiq
|
||||
newrelic_rpm (8.16.0)
|
||||
newrelic_rpm (9.6.0)
|
||||
base64
|
||||
nio4r (2.5.9)
|
||||
nokogiri (1.15.4)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
@ -553,10 +558,12 @@ GEM
|
||||
pundit (2.3.0)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.7.1)
|
||||
racc (1.7.3)
|
||||
rack (2.2.8)
|
||||
rack-attack (6.7.0)
|
||||
rack (>= 1.0, < 4)
|
||||
rack-contrib (2.4.0)
|
||||
rack (< 4)
|
||||
rack-cors (2.0.1)
|
||||
rack (>= 2.0.0)
|
||||
rack-mini-profiler (3.1.1)
|
||||
@ -597,13 +604,13 @@ GEM
|
||||
thor (~> 1.0)
|
||||
zeitwerk (~> 2.5)
|
||||
rainbow (3.1.1)
|
||||
rake (13.0.6)
|
||||
rake (13.1.0)
|
||||
rb-fsevent (0.11.2)
|
||||
rb-inotify (0.10.1)
|
||||
ffi (~> 1.0)
|
||||
redis (5.0.6)
|
||||
redis-client (>= 0.9.0)
|
||||
redis-client (0.17.0)
|
||||
redis-client (0.18.0)
|
||||
connection_pool
|
||||
redis-namespace (1.10.0)
|
||||
redis (>= 4)
|
||||
@ -702,23 +709,23 @@ GEM
|
||||
activesupport (>= 4)
|
||||
selectize-rails (0.12.6)
|
||||
semantic_range (3.0.0)
|
||||
sentry-rails (5.12.0)
|
||||
sentry-rails (5.13.0)
|
||||
railties (>= 5.0)
|
||||
sentry-ruby (~> 5.12.0)
|
||||
sentry-ruby (5.12.0)
|
||||
sentry-ruby (~> 5.13.0)
|
||||
sentry-ruby (5.13.0)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
sentry-sidekiq (5.12.0)
|
||||
sentry-ruby (~> 5.12.0)
|
||||
sentry-sidekiq (5.13.0)
|
||||
sentry-ruby (~> 5.13.0)
|
||||
sidekiq (>= 3.0)
|
||||
sexp_processor (4.17.0)
|
||||
shoulda-matchers (5.3.0)
|
||||
activesupport (>= 5.2.0)
|
||||
sidekiq (7.1.6)
|
||||
sidekiq (7.2.0)
|
||||
concurrent-ruby (< 2)
|
||||
connection_pool (>= 2.3.0)
|
||||
rack (>= 2.2.4)
|
||||
redis-client (>= 0.14.0)
|
||||
sidekiq-cron (1.10.1)
|
||||
sidekiq-cron (1.11.0)
|
||||
fugit (~> 1.8)
|
||||
globalid (>= 1.0.1)
|
||||
sidekiq (>= 6)
|
||||
@ -758,7 +765,7 @@ GEM
|
||||
stripe (8.5.0)
|
||||
telephone_number (1.4.20)
|
||||
test-prof (1.2.1)
|
||||
thor (1.2.2)
|
||||
thor (1.3.0)
|
||||
tilt (2.2.0)
|
||||
time_diff (0.3.0)
|
||||
activesupport
|
||||
@ -838,7 +845,7 @@ DEPENDENCIES
|
||||
administrate-field-belongs_to_search
|
||||
annotate
|
||||
attr_extras
|
||||
audited (~> 5.4, >= 5.4.0)
|
||||
audited (~> 5.4, >= 5.4.1)
|
||||
aws-sdk-s3
|
||||
azure-storage-blob!
|
||||
barnes
|
||||
@ -893,9 +900,10 @@ DEPENDENCIES
|
||||
listen
|
||||
lograge (~> 0.14.0)
|
||||
maxminddb
|
||||
meta_request
|
||||
mock_redis
|
||||
neighbor
|
||||
newrelic-sidekiq-metrics
|
||||
newrelic-sidekiq-metrics (>= 1.6.2)
|
||||
newrelic_rpm
|
||||
omniauth
|
||||
omniauth-google-oauth2
|
||||
@ -927,12 +935,12 @@ DEPENDENCIES
|
||||
scout_apm
|
||||
scss_lint
|
||||
seed_dump
|
||||
sentry-rails (>= 5.12.0)
|
||||
sentry-rails (>= 5.13.0)
|
||||
sentry-ruby
|
||||
sentry-sidekiq (>= 5.12.0)
|
||||
sentry-sidekiq (>= 5.13.0)
|
||||
shoulda-matchers
|
||||
sidekiq (>= 7.1.3)
|
||||
sidekiq-cron (>= 1.10.1)
|
||||
sidekiq-cron (>= 1.11.0)
|
||||
simplecov (= 0.17.1)
|
||||
slack-ruby-client (~> 2.2.0)
|
||||
spring
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
backend: RAILS_ENV=test bin/rails s -p 5050
|
||||
frontend: bin/webpack-dev-server
|
||||
worker: dotenv RAILS_ENV=test bundle exec sidekiq -C config/sidekiq.yml
|
||||
frontend: export NODE_OPTIONS=--openssl-legacy-provider && bin/webpack-dev-server
|
||||
worker: RAILS_ENV=test dotenv bundle exec sidekiq -C config/sidekiq.yml
|
||||
|
||||
21
README.md
21
README.md
@ -1,10 +1,12 @@
|
||||
<p align="center">
|
||||
<img src="https://s3.us-west-2.amazonaws.com/gh-assets.chatwoot.com/brand.svg" alt="Woot-logo" width="240" />
|
||||
<img src="https://user-images.githubusercontent.com/2246121/282256557-1570674b-d142-4198-9740-69404cc6a339.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/>
|
||||
<img src="https://user-images.githubusercontent.com/2246121/282256632-87f6a01b-6467-4e0e-8a93-7bbf66d03a17.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
|
||||
|
||||
<p align="center">Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.</p>
|
||||
</p>
|
||||
___
|
||||
|
||||
<p align="center">
|
||||
# Chatwoot
|
||||
|
||||
Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.
|
||||
<p>
|
||||
<a href="https://heroku.com/deploy?template=https://github.com/chatwoot/chatwoot/tree/master" alt="Deploy to Heroku">
|
||||
<img width="150" alt="Deploy" src="https://www.herokucdn.com/deploy/button.svg"/>
|
||||
</a>
|
||||
@ -13,9 +15,7 @@
|
||||
</a>
|
||||
</p>
|
||||
|
||||
___
|
||||
|
||||
<p align="center">
|
||||
<p>
|
||||
<a href="https://codeclimate.com/github/chatwoot/chatwoot/maintainability"><img src="https://api.codeclimate.com/v1/badges/e6e3f66332c91e5a4c0c/maintainability" alt="Maintainability"></a>
|
||||
<img src="https://img.shields.io/circleci/build/github/chatwoot/chatwoot" alt="CircleCI Badge">
|
||||
<a href="https://hub.docker.com/r/chatwoot/chatwoot/"><img src="https://img.shields.io/docker/pulls/chatwoot/chatwoot" alt="Docker Pull Badge"></a>
|
||||
@ -29,9 +29,8 @@ ___
|
||||
<a href="https://artifacthub.io/packages/helm/chatwoot/chatwoot"><img src="https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/artifact-hub" alt="Artifact HUB"></a>
|
||||
</p>
|
||||
|
||||
<img src="https://chatwoot-public-assets.s3.amazonaws.com/github/screenshot.png" width="100%" alt="Chat dashboard"/>
|
||||
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/2246121/282255783-ee8a50c9-f42d-4752-8201-2d59965a663d.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/>
|
||||
<img src="https://user-images.githubusercontent.com/2246121/282255784-3d1994ec-d895-4ff5-ac68-d819987e1869.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
|
||||
|
||||
Chatwoot is an open-source, self-hosted customer engagement suite. Chatwoot lets you view and manage your customer data, communicate with them irrespective of which medium they use, and re-engage them based on their profile.
|
||||
|
||||
|
||||
@ -76,6 +76,10 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||
message[:reply_to][:story] if message[:reply_to].present? && message[:reply_to][:story].present?
|
||||
end
|
||||
|
||||
def message_reply_attributes
|
||||
message[:reply_to][:mid] if message[:reply_to].present? && message[:reply_to][:mid].present?
|
||||
end
|
||||
|
||||
def build_message
|
||||
return if @outgoing_echo && already_sent_from_chatwoot?
|
||||
return if message_content.blank? && all_unsupported_files?
|
||||
@ -118,7 +122,10 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||
message_type: message_type,
|
||||
source_id: message_identifier,
|
||||
content: message_content,
|
||||
sender: @outgoing_echo ? nil : contact
|
||||
sender: @outgoing_echo ? nil : contact,
|
||||
content_attributes: {
|
||||
in_reply_to_external_id: message_reply_attributes
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@ -9,11 +9,11 @@ class Messages::MessageBuilder
|
||||
@user = user
|
||||
@message_type = params[:message_type] || 'outgoing'
|
||||
@attachments = params[:attachments]
|
||||
@automation_rule = @params&.dig(:content_attributes, :automation_rule_id)
|
||||
@automation_rule = content_attributes&.dig(:automation_rule_id)
|
||||
return unless params.instance_of?(ActionController::Parameters)
|
||||
|
||||
@in_reply_to = params.to_unsafe_h&.dig(:content_attributes, :in_reply_to)
|
||||
@items = params.to_unsafe_h&.dig(:content_attributes, :items)
|
||||
@in_reply_to = content_attributes&.dig(:in_reply_to)
|
||||
@items = content_attributes&.dig(:items)
|
||||
end
|
||||
|
||||
def perform
|
||||
@ -26,6 +26,38 @@ class Messages::MessageBuilder
|
||||
|
||||
private
|
||||
|
||||
# Extracts content attributes from the given params.
|
||||
# - Converts ActionController::Parameters to a regular hash if needed.
|
||||
# - Attempts to parse a JSON string if content is a string.
|
||||
# - Returns an empty hash if content is not present, if there's a parsing error, or if it's an unexpected type.
|
||||
def content_attributes
|
||||
params = convert_to_hash(@params)
|
||||
content_attributes = params.fetch(:content_attributes, {})
|
||||
|
||||
return parse_json(content_attributes) if content_attributes.is_a?(String)
|
||||
return content_attributes if content_attributes.is_a?(Hash)
|
||||
|
||||
{}
|
||||
end
|
||||
|
||||
# Converts the given object to a hash.
|
||||
# If it's an instance of ActionController::Parameters, converts it to an unsafe hash.
|
||||
# Otherwise, returns the object as-is.
|
||||
def convert_to_hash(obj)
|
||||
return obj.to_unsafe_h if obj.instance_of?(ActionController::Parameters)
|
||||
|
||||
obj
|
||||
end
|
||||
|
||||
# Attempts to parse a string as JSON.
|
||||
# If successful, returns the parsed hash with symbolized names.
|
||||
# If unsuccessful, returns nil.
|
||||
def parse_json(content)
|
||||
JSON.parse(content, symbolize_names: true)
|
||||
rescue JSON::ParserError
|
||||
{}
|
||||
end
|
||||
|
||||
def process_attachments
|
||||
return if @attachments.blank?
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
||||
@articles_count = @all_articles.count
|
||||
|
||||
@articles = if list_params[:category_slug].present?
|
||||
@all_articles.order_by_position.page(@current_page).per(50)
|
||||
@all_articles.order_by_position.page(@current_page)
|
||||
else
|
||||
@all_articles.order_by_updated_at.page(@current_page)
|
||||
end
|
||||
|
||||
@ -48,7 +48,7 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle
|
||||
|
||||
def category_params
|
||||
params.require(:category).permit(
|
||||
:name, :description, :position, :slug, :locale, :parent_category_id, :associated_category_id
|
||||
:name, :description, :position, :slug, :locale, :icon, :parent_category_id, :associated_category_id
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@ -47,6 +47,11 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
||||
head :ok
|
||||
end
|
||||
|
||||
def logo
|
||||
@portal.logo.purge if @portal.logo.attached?
|
||||
head :ok
|
||||
end
|
||||
|
||||
def process_attached_logo
|
||||
blob_id = params[:blob_id]
|
||||
blob = ActiveStorage::Blob.find_by(id: blob_id)
|
||||
|
||||
@ -79,6 +79,9 @@ class Api::V1::Widget::BaseController < ApplicationController
|
||||
sender: @contact,
|
||||
content: permitted_params[:message][:content],
|
||||
inbox_id: conversation.inbox_id,
|
||||
content_attributes: {
|
||||
in_reply_to: permitted_params[:message][:reply_to]
|
||||
},
|
||||
echo_id: permitted_params[:message][:echo_id],
|
||||
message_type: :incoming
|
||||
}
|
||||
|
||||
@ -28,7 +28,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||
|
||||
conversation.contact_last_seen_at = DateTime.now.utc
|
||||
conversation.save!
|
||||
::Conversations::MarkMessagesAsReadJob.perform_later(conversation)
|
||||
::Conversations::UpdateMessageStatusJob.perform_later(conversation.id, conversation.contact_last_seen_at)
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
||||
@ -64,7 +64,7 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||
|
||||
def permitted_params
|
||||
# timestamp parameter is used in create conversation method
|
||||
params.permit(:id, :before, :after, :website_token, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id])
|
||||
params.permit(:id, :before, :after, :website_token, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id, :reply_to])
|
||||
end
|
||||
|
||||
def set_message
|
||||
|
||||
@ -23,6 +23,7 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
|
||||
def update_last_seen
|
||||
@conversation.contact_last_seen_at = DateTime.now.utc
|
||||
@conversation.save!
|
||||
::Conversations::UpdateMessageStatusJob.perform_later(@conversation.id, @conversation.contact_last_seen_at)
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
||||
@ -39,10 +39,6 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
|
||||
)
|
||||
end
|
||||
|
||||
def portal
|
||||
@portal ||= Portal.find_by!(slug: permitted_params[:slug], archived: false)
|
||||
end
|
||||
|
||||
def list_params
|
||||
params.permit(:query, :locale, :sort, :status)
|
||||
end
|
||||
|
||||
@ -14,10 +14,14 @@ class Public::Api::V1::Portals::BaseController < PublicController
|
||||
@theme = if %w[dark light].include?(params[:theme])
|
||||
params[:theme]
|
||||
else
|
||||
''
|
||||
'system'
|
||||
end
|
||||
end
|
||||
|
||||
def portal
|
||||
@portal ||= Portal.find_by!(slug: params[:slug], archived: false)
|
||||
end
|
||||
|
||||
def set_locale(&)
|
||||
switch_locale_with_portal(&) if params[:locale].present?
|
||||
switch_locale_with_article(&) if params[:article_slug].present?
|
||||
@ -40,6 +44,8 @@ class Public::Api::V1::Portals::BaseController < PublicController
|
||||
|
||||
def switch_locale_with_article(&)
|
||||
article = Article.find_by(slug: params[:article_slug])
|
||||
Rails.logger.info "Article: not found for slug: #{params[:article_slug]}"
|
||||
render_404 && return if article.blank?
|
||||
|
||||
@locale = if article.category.present?
|
||||
article.category.locale
|
||||
@ -53,4 +59,9 @@ class Public::Api::V1::Portals::BaseController < PublicController
|
||||
def allow_iframe_requests
|
||||
response.headers.delete('X-Frame-Options') if @is_plain_layout_enabled
|
||||
end
|
||||
|
||||
def render_404
|
||||
portal
|
||||
render 'public/api/v1/portals/error/404', status: :not_found
|
||||
end
|
||||
end
|
||||
|
||||
@ -13,10 +13,9 @@ class Public::Api::V1::Portals::CategoriesController < Public::Api::V1::Portals:
|
||||
private
|
||||
|
||||
def set_category
|
||||
@category = @portal.categories.find_by!(locale: params[:locale], slug: params[:category_slug])
|
||||
end
|
||||
@category = @portal.categories.find_by(locale: params[:locale], slug: params[:category_slug])
|
||||
|
||||
def portal
|
||||
@portal ||= Portal.find_by!(slug: params[:slug], archived: false)
|
||||
Rails.logger.info "Category: not found for slug: #{params[:category_slug]}"
|
||||
render_404 && return if @category.blank?
|
||||
end
|
||||
end
|
||||
|
||||
@ -1,11 +1,48 @@
|
||||
module PortalHelper
|
||||
def generate_portal_bg_color(portal_color, theme)
|
||||
base_color = theme == 'dark' ? 'black' : 'white'
|
||||
"color-mix(in srgb, #{portal_color} 10%, #{base_color})"
|
||||
"color-mix(in srgb, #{portal_color} 20%, #{base_color})"
|
||||
end
|
||||
|
||||
def generate_portal_bg(portal_color, theme)
|
||||
bg_image = theme == 'dark' ? 'grid_dark.svg' : 'grid.svg'
|
||||
bg_image = theme == 'dark' ? 'hexagon-dark.svg' : 'hexagon-light.svg'
|
||||
"background: url(/assets/images/hc/#{bg_image}) #{generate_portal_bg_color(portal_color, theme)}"
|
||||
end
|
||||
|
||||
def language_name(locale)
|
||||
language_map = YAML.load_file(Rails.root.join('config/languages/language_map.yml'))
|
||||
language_map[locale] || locale
|
||||
end
|
||||
|
||||
def get_theme_names(theme)
|
||||
if theme == 'light'
|
||||
I18n.t('public_portal.header.appearance.light')
|
||||
elsif theme == 'dark'
|
||||
I18n.t('public_portal.header.appearance.dark')
|
||||
else
|
||||
I18n.t('public_portal.header.appearance.system')
|
||||
end
|
||||
end
|
||||
|
||||
def get_theme_icon(theme)
|
||||
if theme == 'light'
|
||||
'icons/sun'
|
||||
elsif theme == 'dark'
|
||||
'icons/moon'
|
||||
else
|
||||
'icons/monitor'
|
||||
end
|
||||
end
|
||||
|
||||
def generate_gradient_to_bottom(theme)
|
||||
"background-image: linear-gradient(to bottom, transparent, #{theme == 'dark' ? '#151718' : 'white'})"
|
||||
end
|
||||
|
||||
def generate_article_link(portal_slug, article_slug, theme)
|
||||
"/hc/#{portal_slug}/articles/#{article_slug}#{theme.present? && theme != 'system' ? "?theme=#{theme}" : ''}"
|
||||
end
|
||||
|
||||
def render_category_content(content)
|
||||
ChatwootMarkdownRenderer.new(content).render_markdown_to_plain_text
|
||||
end
|
||||
end
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
/* global axios */
|
||||
|
||||
import PortalsAPI from './portals';
|
||||
import { getArticleSearchURL } from 'dashboard/helper/URLHelper.js';
|
||||
|
||||
class ArticlesAPI extends PortalsAPI {
|
||||
constructor() {
|
||||
@ -12,14 +13,31 @@ class ArticlesAPI extends PortalsAPI {
|
||||
portalSlug,
|
||||
locale,
|
||||
status,
|
||||
author_id,
|
||||
category_slug,
|
||||
authorId,
|
||||
categorySlug,
|
||||
sort,
|
||||
}) {
|
||||
let baseUrl = `${this.url}/${portalSlug}/articles?page=${pageNumber}&locale=${locale}`;
|
||||
if (status !== undefined) baseUrl += `&status=${status}`;
|
||||
if (author_id) baseUrl += `&author_id=${author_id}`;
|
||||
if (category_slug) baseUrl += `&category_slug=${category_slug}`;
|
||||
return axios.get(baseUrl);
|
||||
const url = getArticleSearchURL({
|
||||
pageNumber,
|
||||
portalSlug,
|
||||
locale,
|
||||
status,
|
||||
authorId,
|
||||
categorySlug,
|
||||
sort,
|
||||
host: this.url,
|
||||
});
|
||||
|
||||
return axios.get(url);
|
||||
}
|
||||
|
||||
searchArticles({ portalSlug, query }) {
|
||||
const url = getArticleSearchURL({
|
||||
portalSlug,
|
||||
query,
|
||||
host: this.url,
|
||||
});
|
||||
return axios.get(url);
|
||||
}
|
||||
|
||||
getArticle({ id, portalSlug }) {
|
||||
|
||||
@ -17,6 +17,10 @@ class PortalsAPI extends ApiClient {
|
||||
deletePortal(portalSlug) {
|
||||
return axios.delete(`${this.url}/${portalSlug}`);
|
||||
}
|
||||
|
||||
deleteLogo(portalSlug) {
|
||||
return axios.delete(`${this.url}/${portalSlug}/logo`);
|
||||
}
|
||||
}
|
||||
|
||||
export default PortalsAPI;
|
||||
|
||||
@ -26,9 +26,13 @@ export const buildCreatePayload = ({
|
||||
payload.append('echo_id', echoId);
|
||||
payload.append('cc_emails', ccEmails);
|
||||
payload.append('bcc_emails', bccEmails);
|
||||
|
||||
if (toEmails) {
|
||||
payload.append('to_emails', toEmails);
|
||||
}
|
||||
if (contentAttributes) {
|
||||
payload.append('content_attributes', JSON.stringify(contentAttributes));
|
||||
}
|
||||
} else {
|
||||
payload = {
|
||||
content: message,
|
||||
|
||||
@ -34,7 +34,7 @@ describe('#PortalAPI', () => {
|
||||
portalSlug: 'room-rental',
|
||||
locale: 'en-US',
|
||||
status: 'published',
|
||||
author_id: '1',
|
||||
authorId: '1',
|
||||
});
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/portals/room-rental/articles?page=1&locale=en-US&status=published&author_id=1'
|
||||
@ -85,6 +85,33 @@ describe('#PortalAPI', () => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#searchArticles', () => {
|
||||
articlesAPI.searchArticles({
|
||||
query: 'test',
|
||||
portalSlug: 'room-rental',
|
||||
});
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/portals/room-rental/articles?query=test'
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#updateArticle', () => {
|
||||
articlesAPI.updateArticle({
|
||||
articleId: 1,
|
||||
|
||||
@ -50,6 +50,7 @@ describe('#ConversationAPI', () => {
|
||||
message: 'test content',
|
||||
echoId: 12,
|
||||
isPrivate: true,
|
||||
contentAttributes: { in_reply_to: 12 },
|
||||
files: [new Blob(['test-content'], { type: 'application/pdf' })],
|
||||
});
|
||||
expect(formPayload).toBeInstanceOf(FormData);
|
||||
@ -57,6 +58,10 @@ describe('#ConversationAPI', () => {
|
||||
expect(formPayload.get('echo_id')).toEqual('12');
|
||||
expect(formPayload.get('private')).toEqual('true');
|
||||
expect(formPayload.get('cc_emails')).toEqual('');
|
||||
expect(formPayload.get('bcc_emails')).toEqual('');
|
||||
expect(formPayload.get('content_attributes')).toEqual(
|
||||
'{"in_reply_to":12}'
|
||||
);
|
||||
});
|
||||
|
||||
it('builds object payload if file is not available', () => {
|
||||
|
||||
@ -1,9 +1,5 @@
|
||||
@import '~vue2-datepicker/scss/index';
|
||||
|
||||
.mx-datepicker-popup {
|
||||
@apply z-[99999];
|
||||
}
|
||||
|
||||
.date-picker {
|
||||
&.no-margin {
|
||||
.mx-input {
|
||||
@ -29,10 +25,52 @@
|
||||
.mx-input[readonly] {
|
||||
@apply bg-white dark:bg-slate-900 cursor-pointer;
|
||||
}
|
||||
|
||||
.mx-icon-calendar {
|
||||
@apply dark:text-slate-500;
|
||||
}
|
||||
}
|
||||
|
||||
.mx-calendar-content .cell:hover {
|
||||
@apply bg-slate-75 dark:bg-slate-700 text-slate-900 dark:text-slate-100;
|
||||
.mx-datepicker-main {
|
||||
@apply border-0 bg-white dark:bg-slate-800;
|
||||
|
||||
.cell {
|
||||
&.disabled {
|
||||
@apply bg-slate-25 dark:bg-slate-900 text-slate-200 dark:text-slate-300;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.hover-in-range,
|
||||
&.in-range {
|
||||
@apply bg-slate-75 dark:bg-slate-700 text-slate-900 dark:text-slate-100;
|
||||
}
|
||||
}
|
||||
|
||||
.mx-time {
|
||||
@apply border-0 bg-white dark:bg-slate-800;
|
||||
|
||||
.mx-time-header {
|
||||
@apply border-0;
|
||||
}
|
||||
|
||||
.mx-time-item {
|
||||
&.disabled {
|
||||
@apply bg-slate-25 dark:bg-slate-900;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply bg-slate-75 dark:bg-slate-700;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.today {
|
||||
@apply font-semibold;
|
||||
}
|
||||
}
|
||||
|
||||
.mx-datepicker-popup {
|
||||
@apply z-[99999];
|
||||
}
|
||||
|
||||
.mx-datepicker-inline {
|
||||
@ -41,24 +79,4 @@
|
||||
.mx-calendar {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
.cell.disabled {
|
||||
@apply bg-slate-25 dark:bg-slate-900 text-slate-200 dark:text-slate-300;
|
||||
}
|
||||
|
||||
.mx-time-item.disabled {
|
||||
@apply bg-slate-25 dark:bg-slate-900;
|
||||
}
|
||||
|
||||
.today {
|
||||
@apply font-semibold;
|
||||
}
|
||||
|
||||
.mx-datepicker-main {
|
||||
@apply border-0 bg-white dark:bg-slate-800;
|
||||
}
|
||||
|
||||
.mx-time-header {
|
||||
@apply border-0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,3 +16,23 @@
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.integration-hooks {
|
||||
.formulate-input[data-type='checkbox'] {
|
||||
.formulate-input-wrapper {
|
||||
@apply flex;
|
||||
|
||||
.formulate-input-element {
|
||||
@apply pr-2;
|
||||
|
||||
input {
|
||||
@apply mb-0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.formulate-input-element-decorator {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -207,6 +207,7 @@ import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCust
|
||||
import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import filterMixin from 'shared/mixins/filterMixin';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||
import countries from 'shared/constants/countries';
|
||||
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
|
||||
@ -238,6 +239,7 @@ export default {
|
||||
eventListenerMixins,
|
||||
alertMixin,
|
||||
filterMixin,
|
||||
uiSettingsMixin,
|
||||
],
|
||||
props: {
|
||||
conversationInbox: {
|
||||
@ -408,8 +410,12 @@ export default {
|
||||
},
|
||||
conversationListPagination() {
|
||||
const conversationsPerPage = 25;
|
||||
const hasChatsOnView =
|
||||
this.chatsOnView &&
|
||||
Array.isArray(this.chatsOnView) &&
|
||||
!this.chatsOnView.length;
|
||||
const isNoFiltersOrFoldersAndChatListNotEmpty =
|
||||
!this.hasAppliedFiltersOrActiveFolders && this.chatsOnView !== [];
|
||||
!this.hasAppliedFiltersOrActiveFolders && hasChatsOnView;
|
||||
const isUnderPerPage =
|
||||
this.chatsOnView.length < conversationsPerPage &&
|
||||
this.activeAssigneeTabCount < conversationsPerPage &&
|
||||
@ -515,6 +521,7 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setFiltersFromUISettings();
|
||||
this.$store.dispatch('setChatStatusFilter', this.activeStatus);
|
||||
this.$store.dispatch('setChatSortFilter', this.activeSortBy);
|
||||
this.resetAndFetchData();
|
||||
@ -544,6 +551,12 @@ export default {
|
||||
this.$store.dispatch('customViews/update', payloadData);
|
||||
this.closeAdvanceFiltersModal();
|
||||
},
|
||||
setFiltersFromUISettings() {
|
||||
const { conversations_filter_by: filterBy = {} } = this.uiSettings;
|
||||
const { status, order_by: orderBy } = filterBy;
|
||||
this.activeStatus = status || wootConstants.STATUS_TYPE.OPEN;
|
||||
this.activeSortBy = orderBy || wootConstants.SORT_BY_TYPE.LATEST;
|
||||
},
|
||||
onClickOpenAddFoldersModal() {
|
||||
this.showAddFoldersModal = true;
|
||||
},
|
||||
|
||||
@ -136,6 +136,7 @@ export default {
|
||||
toStateName: 'labels_list',
|
||||
showModalForNewItem: true,
|
||||
modalName: 'AddLabel',
|
||||
dataTestid: 'sidebar-new-label-button',
|
||||
children: this.labels.map(label => ({
|
||||
id: label.id,
|
||||
label: label.title,
|
||||
|
||||
@ -78,6 +78,7 @@
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
icon="add"
|
||||
:data-testid="menuItem.dataTestid"
|
||||
@click="e => newLinkClick(e, navigate)"
|
||||
>
|
||||
{{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }}
|
||||
|
||||
@ -61,6 +61,7 @@ export default {
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentChat: 'getSelectedChat',
|
||||
isAChatwootInstance: 'globalConfig/isAChatwootInstance',
|
||||
}),
|
||||
isAICTAModalDismissed() {
|
||||
return this.uiSettings.is_open_ai_cta_modal_dismissed;
|
||||
@ -70,7 +71,8 @@ export default {
|
||||
return (
|
||||
this.isAdmin &&
|
||||
!this.isAIIntegrationEnabled &&
|
||||
!this.isAICTAModalDismissed
|
||||
!this.isAICTAModalDismissed &&
|
||||
this.isAChatwootInstance
|
||||
);
|
||||
},
|
||||
// Display a AI CTA button for agents and other admins who have not yet opened the AI assistance modal.
|
||||
|
||||
@ -135,6 +135,7 @@ export default {
|
||||
// allowSignature is a kill switch, ensuring no signature methods
|
||||
// are triggered except when this flag is true
|
||||
allowSignature: { type: Boolean, default: false },
|
||||
channelType: { type: String, default: '' },
|
||||
showImageResizeToolbar: { type: Boolean, default: false }, // A kill switch to show or hide the image toolbar
|
||||
},
|
||||
data() {
|
||||
@ -266,8 +267,11 @@ export default {
|
||||
sendWithSignature() {
|
||||
// this is considered the source of truth, we watch this property
|
||||
// on change, we toggle the signature in the editor
|
||||
const { send_with_signature: isEnabled } = this.uiSettings;
|
||||
return isEnabled && this.allowSignature && !this.isPrivate;
|
||||
if (this.allowSignature && !this.isPrivate && this.channelType) {
|
||||
return this.fetchSignatureFlagFromUiSettings(this.channelType);
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
@ -442,7 +446,7 @@ export default {
|
||||
this.onFocus();
|
||||
},
|
||||
click: () => {
|
||||
// this.isEditorMouseFocusedOnAnImage(); Enable it when the backend supports for message resize is done.
|
||||
this.isEditorMouseFocusedOnAnImage();
|
||||
},
|
||||
blur: () => {
|
||||
this.onBlur();
|
||||
@ -670,7 +674,7 @@ export default {
|
||||
() => this.resetTyping(),
|
||||
TYPING_INDICATOR_IDLE_TIME
|
||||
);
|
||||
// this.updateImgToolbarOnDelete(); Enable it when the backend supports for message resize is done.
|
||||
this.updateImgToolbarOnDelete();
|
||||
},
|
||||
onKeydown(event) {
|
||||
if (this.isEnterToSendEnabled()) {
|
||||
|
||||
@ -101,7 +101,7 @@
|
||||
<transition name="modal-fade">
|
||||
<div
|
||||
v-show="$refs.upload && $refs.upload.dropActive"
|
||||
class="flex items-center justify-center gap-2 fixed left-0 right-0 top-0 bottom-0 w-full h-full z-20 text-slate-600 dark:text-slate-200 bg-white_transparent dark:bg-black_transparent flex-col"
|
||||
class="fixed top-0 bottom-0 left-0 right-0 z-20 flex flex-col items-center justify-center w-full h-full gap-2 text-slate-600 dark:text-slate-200 bg-white_transparent dark:bg-black_transparent"
|
||||
>
|
||||
<fluent-icon icon="cloud-backup" size="40" />
|
||||
<h4 class="page-sub-title text-slate-600 dark:text-slate-200">
|
||||
@ -109,6 +109,16 @@
|
||||
</h4>
|
||||
</div>
|
||||
</transition>
|
||||
<woot-button
|
||||
v-if="enableInsertArticleInReply"
|
||||
v-tooltip.top-end="$t('HELP_CENTER.ARTICLE_SEARCH.OPEN_ARTICLE_SEARCH')"
|
||||
icon="document-text-link"
|
||||
color-scheme="secondary"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
:title="$t('HELP_CENTER.ARTICLE_SEARCH.OPEN_ARTICLE_SEARCH')"
|
||||
@click="toggleInsertArticle"
|
||||
/>
|
||||
</div>
|
||||
<div class="right-wrap">
|
||||
<woot-button
|
||||
@ -233,6 +243,10 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
portalSlug: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
@ -299,14 +313,21 @@ export default {
|
||||
return !this.isOnPrivateNote;
|
||||
},
|
||||
sendWithSignature() {
|
||||
const { send_with_signature: isEnabled } = this.uiSettings;
|
||||
return isEnabled;
|
||||
// channelType is sourced from inboxMixin
|
||||
return this.fetchSignatureFlagFromUiSettings(this.channelType);
|
||||
},
|
||||
signatureToggleTooltip() {
|
||||
return this.sendWithSignature
|
||||
? this.$t('CONVERSATION.FOOTER.DISABLE_SIGN_TOOLTIP')
|
||||
: this.$t('CONVERSATION.FOOTER.ENABLE_SIGN_TOOLTIP');
|
||||
},
|
||||
enableInsertArticleInReply() {
|
||||
const isFeatEnabled = this.isFeatureEnabledonAccount(
|
||||
this.accountId,
|
||||
FEATURE_FLAGS.INSERT_ARTICLE_IN_REPLY
|
||||
);
|
||||
return isFeatEnabled && this.portalSlug;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
ActiveStorage.start();
|
||||
@ -318,13 +339,14 @@ export default {
|
||||
}
|
||||
},
|
||||
toggleMessageSignature() {
|
||||
this.updateUISettings({
|
||||
send_with_signature: !this.sendWithSignature,
|
||||
});
|
||||
this.setSignatureFlagForInbox(this.channelType, !this.sendWithSignature);
|
||||
},
|
||||
replaceText(text) {
|
||||
this.$emit('replace-text', text);
|
||||
},
|
||||
toggleInsertArticle() {
|
||||
this.$emit('toggle-insert-article');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -32,7 +32,7 @@
|
||||
}}</span>
|
||||
<filter-item
|
||||
type="sort"
|
||||
:selected-value="chatSortFilter"
|
||||
:selected-value="sortFilter"
|
||||
:items="chatSortItems"
|
||||
path-prefix="CHAT_LIST.CHAT_SORT_FILTER_ITEMS"
|
||||
@onChangeFilter="onChangeFilter"
|
||||
@ -47,12 +47,13 @@ import wootConstants from 'dashboard/constants/globals';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import FilterItem from './FilterItem.vue';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FilterItem,
|
||||
},
|
||||
mixins: [clickaway],
|
||||
mixins: [clickaway, uiSettingsMixin],
|
||||
data() {
|
||||
return {
|
||||
showActionsDropdown: false,
|
||||
@ -83,8 +84,17 @@ export default {
|
||||
closeDropdown() {
|
||||
this.showActionsDropdown = false;
|
||||
},
|
||||
onChangeFilter(type, value) {
|
||||
this.$emit('changeFilter', type, value);
|
||||
onChangeFilter(value, type) {
|
||||
this.$emit('changeFilter', value, type);
|
||||
this.saveSelectedFilter(type, value);
|
||||
},
|
||||
saveSelectedFilter(type, value) {
|
||||
this.updateUISettings({
|
||||
conversations_filter_by: {
|
||||
status: type === 'status' ? value : this.chatStatus,
|
||||
order_by: type === 'sort' ? value : this.sortFilter,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -22,22 +22,12 @@
|
||||
:bcc="emailHeadAttributes.bcc"
|
||||
:is-incoming="isIncoming"
|
||||
/>
|
||||
<blockquote v-if="storyReply" class="story-reply-quote">
|
||||
<span>{{ $t('CONVERSATION.REPLIED_TO_STORY') }}</span>
|
||||
<bubble-image
|
||||
v-if="!hasImgStoryError && storyUrl"
|
||||
:url="storyUrl"
|
||||
@error="onStoryLoadError"
|
||||
/>
|
||||
<bubble-video
|
||||
v-else-if="hasImgStoryError && storyUrl"
|
||||
:url="storyUrl"
|
||||
/>
|
||||
</blockquote>
|
||||
<instagram-story-reply v-if="storyUrl" :story-url="storyUrl" />
|
||||
<bubble-reply-to
|
||||
v-if="inReplyToMessageId && inboxSupportsReplyTo"
|
||||
v-if="inReplyToMessageId && inboxSupportsReplyTo.incoming"
|
||||
:message="inReplyTo"
|
||||
:message-type="data.message_type"
|
||||
:parent-has-attachments="hasAttachments"
|
||||
/>
|
||||
<bubble-text
|
||||
v-if="data.content"
|
||||
@ -58,10 +48,15 @@
|
||||
</span>
|
||||
<div v-if="!isPending && hasAttachments">
|
||||
<div v-for="attachment in attachments" :key="attachment.id">
|
||||
<instagram-story
|
||||
v-if="isAnInstagramStory"
|
||||
:story-url="attachment.data_url"
|
||||
@error="onMediaLoadError"
|
||||
/>
|
||||
<bubble-image-audio-video
|
||||
v-if="isAttachmentImageVideoAudio(attachment.file_type)"
|
||||
v-else-if="isAttachmentImageVideoAudio(attachment.file_type)"
|
||||
:attachment="attachment"
|
||||
@error="onImageLoadError"
|
||||
@error="onMediaLoadError"
|
||||
/>
|
||||
<bubble-location
|
||||
v-else-if="attachment.file_type === 'location'"
|
||||
@ -74,9 +69,6 @@
|
||||
:name="data.content"
|
||||
:phone-number="attachment.fallback_title"
|
||||
/>
|
||||
<instagram-image-error-placeholder
|
||||
v-else-if="hasImageError && hasInstagramStory"
|
||||
/>
|
||||
<bubble-file v-else :url="attachment.data_url" />
|
||||
</div>
|
||||
</div>
|
||||
@ -88,7 +80,6 @@
|
||||
:story-id="`${storyId}`"
|
||||
:is-a-tweet="isATweet"
|
||||
:is-a-whatsapp-channel="isAWhatsAppChannel"
|
||||
:has-instagram-story="hasInstagramStory"
|
||||
:is-email="isEmailContentType"
|
||||
:is-private="data.private"
|
||||
:message-type="data.message_type"
|
||||
@ -137,19 +128,18 @@
|
||||
<script>
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import BubbleActions from './bubble/Actions.vue';
|
||||
import BubbleContact from './bubble/Contact.vue';
|
||||
import BubbleFile from './bubble/File.vue';
|
||||
import BubbleImage from './bubble/Image.vue';
|
||||
import BubbleVideo from './bubble/Video.vue';
|
||||
import BubbleImageAudioVideo from './bubble/ImageAudioVideo.vue';
|
||||
import BubbleIntegration from './bubble/Integration.vue';
|
||||
import BubbleLocation from './bubble/Location.vue';
|
||||
import BubbleMailHead from './bubble/MailHead.vue';
|
||||
import BubbleText from './bubble/Text.vue';
|
||||
import BubbleContact from './bubble/Contact.vue';
|
||||
import BubbleReplyTo from './bubble/ReplyTo.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import BubbleText from './bubble/Text.vue';
|
||||
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu.vue';
|
||||
import instagramImageErrorPlaceholder from './instagramImageErrorPlaceholder.vue';
|
||||
import InstagramStory from './bubble/InstagramStory.vue';
|
||||
import InstagramStoryReply from './bubble/InstagramStoryReply.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import contentTypeMixin from 'shared/mixins/contentTypeMixin';
|
||||
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
|
||||
@ -162,19 +152,18 @@ import { LocalStorage } from 'shared/helpers/localStorage';
|
||||
export default {
|
||||
components: {
|
||||
BubbleActions,
|
||||
BubbleContact,
|
||||
BubbleFile,
|
||||
BubbleImage,
|
||||
BubbleVideo,
|
||||
BubbleImageAudioVideo,
|
||||
BubbleIntegration,
|
||||
BubbleLocation,
|
||||
BubbleMailHead,
|
||||
BubbleText,
|
||||
BubbleContact,
|
||||
BubbleReplyTo,
|
||||
BubbleText,
|
||||
ContextMenu,
|
||||
InstagramStory,
|
||||
InstagramStoryReply,
|
||||
Spinner,
|
||||
instagramImageErrorPlaceholder,
|
||||
},
|
||||
mixins: [alertMixin, messageFormatterMixin, contentTypeMixin],
|
||||
props: {
|
||||
@ -190,17 +179,13 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hasInstagramStory: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isWebWidgetInbox: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
inboxSupportsReplyTo: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
inReplyTo: {
|
||||
type: Object,
|
||||
@ -210,10 +195,9 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
showContextMenu: false,
|
||||
hasImageError: false,
|
||||
hasMediaLoadError: false,
|
||||
contextMenuPosition: {},
|
||||
showBackgroundHighlight: false,
|
||||
hasImgStoryError: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -289,19 +273,22 @@ export default {
|
||||
// since old messages are only loaded when the user scrolls up
|
||||
return this.data.content_attributes?.in_reply_to;
|
||||
},
|
||||
isAnInstagramStory() {
|
||||
return this.contentAttributes.image_type === 'story_mention';
|
||||
},
|
||||
contextMenuEnabledOptions() {
|
||||
return {
|
||||
copy: this.hasText,
|
||||
delete: this.hasText || this.hasAttachments,
|
||||
cannedResponse: this.isOutgoing && this.hasText,
|
||||
replyTo: !this.data.private && this.inboxSupportsReplyTo,
|
||||
replyTo: !this.data.private && this.inboxSupportsReplyTo.outgoing,
|
||||
};
|
||||
},
|
||||
contentAttributes() {
|
||||
return this.data.content_attributes || {};
|
||||
},
|
||||
externalError() {
|
||||
return this.contentAttributes.external_error || null;
|
||||
return this.contentAttributes.external_error || '';
|
||||
},
|
||||
sender() {
|
||||
return this.data.sender || {};
|
||||
@ -318,9 +305,6 @@ export default {
|
||||
storyUrl() {
|
||||
return this.contentAttributes.story_url || null;
|
||||
},
|
||||
storyReply() {
|
||||
return this.storyUrl && this.hasInstagramStory;
|
||||
},
|
||||
contentType() {
|
||||
const {
|
||||
data: { content_type: contentType },
|
||||
@ -401,7 +385,7 @@ export default {
|
||||
return false;
|
||||
}
|
||||
if (this.isFailed) {
|
||||
return this.externalError ? '' : this.$t(`CONVERSATION.SEND_FAILED`);
|
||||
return this.externalError || this.$t(`CONVERSATION.SEND_FAILED`);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
@ -459,13 +443,11 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
data() {
|
||||
this.hasImageError = false;
|
||||
this.hasImgStoryError = false;
|
||||
this.hasMediaLoadError = false;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.hasImageError = false;
|
||||
this.hasImgStoryError = false;
|
||||
this.hasMediaLoadError = false;
|
||||
bus.$on(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
|
||||
this.setupHighlightTimer();
|
||||
},
|
||||
@ -475,13 +457,13 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
isAttachmentImageVideoAudio(fileType) {
|
||||
return ['image', 'audio', 'video'].includes(fileType);
|
||||
return ['image', 'audio', 'video', 'story_mention'].includes(fileType);
|
||||
},
|
||||
hasMediaAttachment(type) {
|
||||
if (this.hasAttachments && this.data.attachments.length > 0) {
|
||||
const { attachments = [{}] } = this.data;
|
||||
const { file_type: fileType } = attachments[0];
|
||||
return fileType === type && !this.hasImageError;
|
||||
return fileType === type && !this.hasMediaLoadError;
|
||||
}
|
||||
if (this.storyReply) {
|
||||
return true;
|
||||
@ -494,11 +476,8 @@ export default {
|
||||
async retrySendMessage() {
|
||||
await this.$store.dispatch('sendMessageWithData', this.data);
|
||||
},
|
||||
onImageLoadError() {
|
||||
this.hasImageError = true;
|
||||
},
|
||||
onStoryLoadError() {
|
||||
this.hasImgStoryError = true;
|
||||
onMediaLoadError() {
|
||||
this.hasMediaLoadError = true;
|
||||
},
|
||||
openContextMenu(e) {
|
||||
const shouldSkipContextMenu =
|
||||
@ -730,8 +709,4 @@ li.right {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.story-reply-quote {
|
||||
@apply mt-2 mx-4 mb-0 px-2 pb-0 pt-2 border-l-4 border-solid border-slate-75 dark:border-slate-600 text-slate-600 dark:text-slate-200;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -31,7 +31,6 @@
|
||||
:data="message"
|
||||
:is-a-tweet="isATweet"
|
||||
:is-a-whatsapp-channel="isAWhatsAppChannel"
|
||||
:has-instagram-story="hasInstagramStory"
|
||||
:is-web-widget-inbox="isAWebWidgetInbox"
|
||||
:inbox-supports-reply-to="inboxSupportsReplyTo"
|
||||
:in-reply-to="getInReplyToMessage(message)"
|
||||
@ -54,7 +53,6 @@
|
||||
:data="message"
|
||||
:is-a-tweet="isATweet"
|
||||
:is-a-whatsapp-channel="isAWhatsAppChannel"
|
||||
:has-instagram-story="hasInstagramStory"
|
||||
:is-web-widget-inbox="isAWebWidgetInbox"
|
||||
:inbox-supports-reply-to="inboxSupportsReplyTo"
|
||||
:in-reply-to="getInReplyToMessage(message)"
|
||||
@ -116,7 +114,6 @@ import { LocalStorage } from 'shared/helpers/localStorage';
|
||||
|
||||
// constants
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
import { REPLY_POLICY } from 'shared/constants/links';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||
@ -238,10 +235,6 @@ export default {
|
||||
isATweet() {
|
||||
return this.conversationType === 'tweet';
|
||||
},
|
||||
|
||||
hasInstagramStory() {
|
||||
return this.conversationType === 'instagram_direct_message';
|
||||
},
|
||||
isRightOrLeftIcon() {
|
||||
if (this.isContactPanelOpen) {
|
||||
return 'arrow-chevron-right';
|
||||
@ -291,13 +284,10 @@ export default {
|
||||
return this.currentChat.unread_count || 0;
|
||||
},
|
||||
inboxSupportsReplyTo() {
|
||||
return (
|
||||
this.inboxHasFeature(INBOX_FEATURES.REPLY_TO) &&
|
||||
this.isFeatureEnabledonAccount(
|
||||
this.accountId,
|
||||
FEATURE_FLAGS.MESSAGE_REPLY_TO
|
||||
)
|
||||
);
|
||||
return {
|
||||
incoming: this.inboxHasFeature(INBOX_FEATURES.REPLY_TO),
|
||||
outgoing: this.inboxHasFeature(INBOX_FEATURES.REPLY_TO_OUTGOING),
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
@ -350,7 +340,7 @@ export default {
|
||||
// method available in mixin, need to ensure that integrations are present
|
||||
await this.fetchIntegrationsIfRequired();
|
||||
|
||||
if (!this.isAIIntegrationEnabled) {
|
||||
if (!this.isLabelSuggestionFeatureEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -18,6 +18,12 @@
|
||||
:popout-reply-box="popoutReplyBox"
|
||||
@click="$emit('click')"
|
||||
/>
|
||||
<article-search-popover
|
||||
v-if="showArticleSearchPopover && connectedPortalSlug"
|
||||
:selected-portal-slug="connectedPortalSlug"
|
||||
@insert="handleInsert"
|
||||
@close="onSearchPopoverClose"
|
||||
/>
|
||||
<div class="reply-box__top">
|
||||
<reply-to-message
|
||||
v-if="shouldShowReplyToMessage"
|
||||
@ -35,7 +41,7 @@
|
||||
v-if="showEmojiPicker"
|
||||
v-on-clickaway="hideEmojiPicker"
|
||||
:class="emojiDialogClassOnExpandedLayoutAndRTLView"
|
||||
:on-click="emojiOnClick"
|
||||
:on-click="addIntoEditor"
|
||||
/>
|
||||
<reply-email-head
|
||||
v-if="showReplyHead"
|
||||
@ -79,6 +85,7 @@
|
||||
:variables="messageVariables"
|
||||
:signature="signatureToApply"
|
||||
:allow-signature="true"
|
||||
:channel-type="channelType"
|
||||
@typing-off="onTypingOff"
|
||||
@typing-on="onTypingOn"
|
||||
@focus="onFocus"
|
||||
@ -121,10 +128,12 @@
|
||||
:toggle-audio-recorder="toggleAudioRecorder"
|
||||
:toggle-emoji-picker="toggleEmojiPicker"
|
||||
:message="message"
|
||||
:portal-slug="connectedPortalSlug"
|
||||
:new-conversation-modal-active="newConversationModalActive"
|
||||
@selectWhatsappTemplate="openWhatsappTemplateModal"
|
||||
@toggle-editor="toggleRichContentEditor"
|
||||
@replace-text="replaceText"
|
||||
@toggle-insert-article="toggleInsertArticle"
|
||||
/>
|
||||
<whatsapp-templates
|
||||
:inbox-id="inbox.id"
|
||||
@ -154,6 +163,7 @@ import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview.v
|
||||
import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel.vue';
|
||||
import ReplyEmailHead from './ReplyEmailHead.vue';
|
||||
import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue';
|
||||
import ArticleSearchPopover from 'dashboard/routes/dashboard/helpcenter/components/ArticleSearch/SearchPopover.vue';
|
||||
import MessageSignatureMissingAlert from './MessageSignatureMissingAlert';
|
||||
import Banner from 'dashboard/components/ui/Banner.vue';
|
||||
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
|
||||
@ -184,7 +194,6 @@ import {
|
||||
replaceSignature,
|
||||
extractTextFromMarkdown,
|
||||
} from 'dashboard/helper/editorHelper';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
|
||||
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||
@ -206,6 +215,7 @@ export default {
|
||||
Banner,
|
||||
WhatsappTemplates,
|
||||
MessageSignatureMissingAlert,
|
||||
ArticleSearchPopover,
|
||||
},
|
||||
mixins: [
|
||||
clickaway,
|
||||
@ -248,6 +258,7 @@ export default {
|
||||
showCannedMenu: false,
|
||||
showVariablesMenu: false,
|
||||
newConversationModalActive: false,
|
||||
showArticleSearchPopover: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -264,11 +275,7 @@ export default {
|
||||
return (
|
||||
this.inReplyTo?.id &&
|
||||
!this.isPrivate &&
|
||||
this.inboxHasFeature(INBOX_FEATURES.REPLY_TO) &&
|
||||
this.isFeatureEnabledonAccount(
|
||||
this.accountId,
|
||||
FEATURE_FLAGS.MESSAGE_REPLY_TO
|
||||
)
|
||||
this.inboxHasFeature(INBOX_FEATURES.REPLY_TO)
|
||||
);
|
||||
},
|
||||
showRichContentEditor() {
|
||||
@ -466,8 +473,7 @@ export default {
|
||||
return !!this.signatureToApply;
|
||||
},
|
||||
sendWithSignature() {
|
||||
const { send_with_signature: isEnabled } = this.uiSettings;
|
||||
return isEnabled;
|
||||
return this.fetchSignatureFlagFromUiSettings(this.channelType);
|
||||
},
|
||||
editorMessageKey() {
|
||||
const { editor_message_key: isEnabled } = this.uiSettings;
|
||||
@ -506,11 +512,18 @@ export default {
|
||||
? this.messageSignature
|
||||
: extractTextFromMarkdown(this.messageSignature);
|
||||
},
|
||||
connectedPortalSlug() {
|
||||
const { help_center: portal = {} } = this.inbox;
|
||||
const { slug = '' } = portal;
|
||||
return slug;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentChat(conversation) {
|
||||
const { can_reply: canReply } = conversation;
|
||||
|
||||
this.setCCAndToEmailsFromLastChat();
|
||||
|
||||
if (this.isOnPrivateNote) {
|
||||
return;
|
||||
}
|
||||
@ -521,7 +534,6 @@ export default {
|
||||
this.replyType = REPLY_EDITOR_MODES.NOTE;
|
||||
}
|
||||
|
||||
this.setCCAndToEmailsFromLastChat();
|
||||
this.fetchAndSetReplyTo();
|
||||
},
|
||||
conversationIdByRoute(conversationId, oldConversationId) {
|
||||
@ -531,17 +543,25 @@ export default {
|
||||
}
|
||||
},
|
||||
message(updatedMessage) {
|
||||
this.hasSlashCommand =
|
||||
updatedMessage[0] === '/' && !this.showRichContentEditor;
|
||||
const hasNextWord = updatedMessage.includes(' ');
|
||||
const isShortCodeActive = this.hasSlashCommand && !hasNextWord;
|
||||
if (isShortCodeActive) {
|
||||
this.mentionSearchKey = updatedMessage.substring(1);
|
||||
this.showMentions = true;
|
||||
} else {
|
||||
this.mentionSearchKey = '';
|
||||
this.showMentions = false;
|
||||
}
|
||||
// Check if the message starts with a slash.
|
||||
const bodyWithoutSignature = removeSignature(
|
||||
updatedMessage,
|
||||
this.signatureToApply
|
||||
);
|
||||
const startsWithSlash = bodyWithoutSignature.startsWith('/');
|
||||
|
||||
// Determine if the user is potentially typing a slash command.
|
||||
// This is true if the message starts with a slash and the rich content editor is not active.
|
||||
this.hasSlashCommand = startsWithSlash && !this.showRichContentEditor;
|
||||
this.showMentions = this.hasSlashCommand;
|
||||
|
||||
// If a slash command is active, extract the command text after the slash.
|
||||
// If not, reset the mentionSearchKey.
|
||||
this.mentionSearchKey = this.hasSlashCommand
|
||||
? bodyWithoutSignature.substring(1)
|
||||
: '';
|
||||
|
||||
// Autosave the current message draft.
|
||||
this.doAutoSaveDraft();
|
||||
},
|
||||
replyType(updatedReplyType, oldReplyType) {
|
||||
@ -588,6 +608,23 @@ export default {
|
||||
);
|
||||
},
|
||||
methods: {
|
||||
handleInsert(article) {
|
||||
const { url, title } = article;
|
||||
if (this.isRichEditorEnabled) {
|
||||
// Removing empty lines from the title
|
||||
const lines = title.split('\n');
|
||||
const nonEmptyLines = lines.filter(line => line.trim() !== '');
|
||||
const filteredMarkdown = nonEmptyLines.join(' ');
|
||||
bus.$emit(
|
||||
BUS_EVENTS.INSERT_INTO_RICH_EDITOR,
|
||||
`[${filteredMarkdown}](${url})`
|
||||
);
|
||||
} else {
|
||||
this.addIntoEditor(
|
||||
`${this.$t('CONVERSATION.REPLYBOX.INSERT_READ_MORE')} ${url}`
|
||||
);
|
||||
}
|
||||
},
|
||||
toggleRichContentEditor() {
|
||||
this.updateUISettings({
|
||||
display_rich_content_editor: !this.showRichContentEditor,
|
||||
@ -819,10 +856,18 @@ export default {
|
||||
this.hideWhatsappTemplatesModal();
|
||||
},
|
||||
replaceText(message) {
|
||||
if (this.sendWithSignature && !this.private) {
|
||||
// if signature is enabled, append it to the message
|
||||
// appendSignature ensures that the signature is not duplicated
|
||||
// so we don't need to check if the signature is already present
|
||||
message = appendSignature(message, this.signatureToApply);
|
||||
}
|
||||
|
||||
const updatedMessage = replaceVariablesInMessage({
|
||||
message,
|
||||
variables: this.messageVariables,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.$track(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE);
|
||||
this.message = updatedMessage;
|
||||
@ -845,22 +890,22 @@ export default {
|
||||
clearEditorSelection() {
|
||||
this.updateEditorSelectionWith = '';
|
||||
},
|
||||
insertEmoji(emoji, selectionStart, selectionEnd) {
|
||||
insertIntoTextEditor(text, selectionStart, selectionEnd) {
|
||||
const { message } = this;
|
||||
const newMessage =
|
||||
message.slice(0, selectionStart) +
|
||||
emoji +
|
||||
text +
|
||||
message.slice(selectionEnd, message.length);
|
||||
this.message = newMessage;
|
||||
},
|
||||
emojiOnClick(emoji) {
|
||||
addIntoEditor(content) {
|
||||
if (this.showRichContentEditor) {
|
||||
this.updateEditorSelectionWith = emoji;
|
||||
this.updateEditorSelectionWith = content;
|
||||
this.onFocus();
|
||||
}
|
||||
if (!this.showRichContentEditor) {
|
||||
const { selectionStart, selectionEnd } = this.$refs.messageInput.$el;
|
||||
this.insertEmoji(emoji, selectionStart, selectionEnd);
|
||||
this.insertIntoTextEditor(content, selectionStart, selectionEnd);
|
||||
}
|
||||
},
|
||||
clearMessage() {
|
||||
@ -964,6 +1009,19 @@ export default {
|
||||
(item, index) => itemIndex !== index
|
||||
);
|
||||
},
|
||||
setReplyToInPayload(payload) {
|
||||
if (this.inReplyTo?.id) {
|
||||
return {
|
||||
...payload,
|
||||
contentAttributes: {
|
||||
...payload.contentAttributes,
|
||||
in_reply_to: this.inReplyTo.id,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return payload;
|
||||
},
|
||||
getMessagePayloadForWhatsapp(message) {
|
||||
const multipleMessagePayload = [];
|
||||
|
||||
@ -973,41 +1031,41 @@ export default {
|
||||
const attachedFile = this.globalConfig.directUploadsEnabled
|
||||
? attachment.blobSignedId
|
||||
: attachment.resource.file;
|
||||
const attachmentPayload = {
|
||||
let attachmentPayload = {
|
||||
conversationId: this.currentChat.id,
|
||||
files: [attachedFile],
|
||||
private: false,
|
||||
message: caption,
|
||||
sender: this.sender,
|
||||
};
|
||||
|
||||
attachmentPayload = this.setReplyToInPayload(attachmentPayload);
|
||||
multipleMessagePayload.push(attachmentPayload);
|
||||
caption = '';
|
||||
});
|
||||
} else {
|
||||
const messagePayload = {
|
||||
let messagePayload = {
|
||||
conversationId: this.currentChat.id,
|
||||
message,
|
||||
private: false,
|
||||
sender: this.sender,
|
||||
};
|
||||
|
||||
messagePayload = this.setReplyToInPayload(messagePayload);
|
||||
|
||||
multipleMessagePayload.push(messagePayload);
|
||||
}
|
||||
|
||||
return multipleMessagePayload;
|
||||
},
|
||||
getMessagePayload(message) {
|
||||
const messagePayload = {
|
||||
let messagePayload = {
|
||||
conversationId: this.currentChat.id,
|
||||
message,
|
||||
private: this.isPrivate,
|
||||
sender: this.sender,
|
||||
};
|
||||
|
||||
if (this.inReplyTo?.id) {
|
||||
messagePayload.contentAttributes = {
|
||||
in_reply_to: this.inReplyTo.id,
|
||||
};
|
||||
}
|
||||
messagePayload = this.setReplyToInPayload(messagePayload);
|
||||
|
||||
if (this.attachedFiles && this.attachedFiles.length) {
|
||||
messagePayload.files = [];
|
||||
@ -1106,6 +1164,12 @@ export default {
|
||||
// When new conversation modal is open
|
||||
this.newConversationModalActive = isActive;
|
||||
},
|
||||
onSearchPopoverClose() {
|
||||
this.showArticleSearchPopover = false;
|
||||
},
|
||||
toggleInsertArticle() {
|
||||
this.showArticleSearchPopover = !this.showArticleSearchPopover;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -1124,7 +1188,7 @@ export default {
|
||||
}
|
||||
|
||||
.reply-box {
|
||||
@apply border-t border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-900;
|
||||
@apply relative border-t border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-900;
|
||||
|
||||
&.is-private {
|
||||
@apply bg-yellow-50 dark:bg-yellow-200;
|
||||
|
||||
@ -114,10 +114,6 @@ export default {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
hasInstagramStory: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
messageType: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
@ -196,28 +192,46 @@ export default {
|
||||
if (!this.showStatusIndicators) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Messages will be marked as sent for the Email channel if they have a source ID.
|
||||
if (this.isAnEmailChannel) {
|
||||
return !!this.sourceId;
|
||||
}
|
||||
|
||||
if (this.isAWhatsAppChannel || this.isATwilioChannel) {
|
||||
if (
|
||||
this.isAWhatsAppChannel ||
|
||||
this.isATwilioChannel ||
|
||||
this.isAFacebookInbox ||
|
||||
this.isASmsInbox ||
|
||||
this.isATelegramChannel
|
||||
) {
|
||||
return this.sourceId && this.isSent;
|
||||
}
|
||||
// All messages will be mark as sent for the Line channel, as there is no source ID.
|
||||
if (this.isALineChannel) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
showDeliveredIndicator() {
|
||||
if (!this.showStatusIndicators) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.isAWhatsAppChannel || this.isATwilioChannel) {
|
||||
if (
|
||||
this.isAWhatsAppChannel ||
|
||||
this.isATwilioChannel ||
|
||||
this.isASmsInbox ||
|
||||
this.isAFacebookInbox
|
||||
) {
|
||||
return this.sourceId && this.isDelivered;
|
||||
}
|
||||
// We will consider messages as delivered for web widget inbox if they are sent
|
||||
if (this.isAWebWidgetInbox) {
|
||||
// All messages marked as delivered for the web widget inbox and API inbox once they are sent.
|
||||
if (this.isAWebWidgetInbox || this.isAPIInbox) {
|
||||
return this.isSent;
|
||||
}
|
||||
if (this.isALineChannel) {
|
||||
return this.isDelivered;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
@ -225,14 +239,16 @@ export default {
|
||||
if (!this.showStatusIndicators) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.isAWebWidgetInbox || this.isAPIInbox) {
|
||||
const { contact_last_seen_at: contactLastSeenAt } = this.currentChat;
|
||||
return contactLastSeenAt >= this.createdAt;
|
||||
if (
|
||||
this.isAWhatsAppChannel ||
|
||||
this.isATwilioChannel ||
|
||||
this.isAFacebookInbox
|
||||
) {
|
||||
return this.sourceId && this.isRead;
|
||||
}
|
||||
|
||||
if (this.isAWhatsAppChannel || this.isATwilioChannel) {
|
||||
return this.sourceId && this.isRead;
|
||||
if (this.isAWebWidgetInbox || this.isAPIInbox) {
|
||||
return this.isRead;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="image message-text__wrap">
|
||||
<img :src="url" @click="onClick" @error="onImgError()" />
|
||||
<img :src="url" @click="onClick" @error="$emit('error')" />
|
||||
<woot-modal :full-width="true" :show.sync="show" :on-close="onClose">
|
||||
<img :src="url" class="modal-image skip-context-menu" />
|
||||
</woot-modal>
|
||||
@ -28,9 +28,6 @@ export default {
|
||||
onClick() {
|
||||
this.show = true;
|
||||
},
|
||||
onImgError() {
|
||||
this.$emit('error');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -4,13 +4,14 @@
|
||||
v-if="isImage && !isImageError"
|
||||
:src="attachment.data_url"
|
||||
@click="onClick"
|
||||
@error="onImgError()"
|
||||
@error="onImgError"
|
||||
/>
|
||||
<video
|
||||
v-if="isVideo"
|
||||
:src="attachment.data_url"
|
||||
muted
|
||||
playsInline
|
||||
@error="onImgError"
|
||||
@click="onClick"
|
||||
/>
|
||||
<audio v-else-if="isAudio" controls class="skip-context-menu">
|
||||
@ -21,7 +22,7 @@
|
||||
:show.sync="show"
|
||||
:attachment="attachment"
|
||||
:all-attachments="filteredCurrentChatAttachments"
|
||||
@error="onImgError()"
|
||||
@error="onImgError"
|
||||
@close="onClose"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<bubble-image
|
||||
v-if="!hasImgStoryError"
|
||||
:url="storyUrl"
|
||||
@error="onImageLoadError"
|
||||
/>
|
||||
<bubble-video
|
||||
v-else-if="!hasVideoStoryError"
|
||||
:url="storyUrl"
|
||||
@error="onVideoLoadError"
|
||||
/>
|
||||
<instagram-story-error-place-holder v-else />
|
||||
</template>
|
||||
<script>
|
||||
import BubbleImage from './Image.vue';
|
||||
import BubbleVideo from './Video.vue';
|
||||
import InstagramStoryErrorPlaceHolder from './InstagramStoryErrorPlaceHolder.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BubbleImage,
|
||||
BubbleVideo,
|
||||
InstagramStoryErrorPlaceHolder,
|
||||
},
|
||||
props: {
|
||||
storyUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hasImgStoryError: false,
|
||||
hasVideoStoryError: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onImageLoadError() {
|
||||
this.hasImgStoryError = true;
|
||||
this.emitError();
|
||||
},
|
||||
onVideoLoadError() {
|
||||
this.hasVideoStoryError = true;
|
||||
this.emitError();
|
||||
},
|
||||
emitError() {
|
||||
this.$emit('error');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-center px-8 h-28 w-full bg-slate-100 text-slate-700 dark:bg-slate-500 dark:text-slate-75"
|
||||
>
|
||||
<fluent-icon icon="document-error" size="32" />
|
||||
<p class="mb-0 text-slate-700 dark:text-slate-75">
|
||||
{{ $t('COMPONENTS.FILE_BUBBLE.INSTAGRAM_STORY_UNAVAILABLE') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {};
|
||||
</script>
|
||||
@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<blockquote
|
||||
class="my-0 px-2 pb-0 pt-0 border-l-4 border-solid border-slate-75 dark:border-slate-600 text-slate-600 dark:text-slate-200"
|
||||
>
|
||||
<span>{{ $t('CONVERSATION.REPLIED_TO_STORY') }}</span>
|
||||
<instagram-story :story-url="storyUrl" class="mt-3 rounded-md" />
|
||||
</blockquote>
|
||||
</template>
|
||||
<script>
|
||||
import InstagramStory from './InstagramStory.vue';
|
||||
|
||||
export default {
|
||||
components: { InstagramStory },
|
||||
props: {
|
||||
storyUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hasImgStoryError: false,
|
||||
hasVideoStoryError: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onImageLoadError() {
|
||||
this.hasImgStoryError = true;
|
||||
},
|
||||
onVideoLoadError() {
|
||||
this.hasVideoStoryError = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -1,10 +1,11 @@
|
||||
<template>
|
||||
<div
|
||||
class="px-2 py-1.5 -mx-2 rounded-sm min-w-[15rem] mb-2"
|
||||
class="px-2 py-1.5 rounded-sm min-w-[10rem] mb-2"
|
||||
:class="{
|
||||
'bg-slate-100 dark:bg-slate-600 dark:text-slate-50':
|
||||
'bg-slate-50 dark:bg-slate-600 dark:text-slate-50':
|
||||
messageType === MESSAGE_TYPE.INCOMING,
|
||||
'bg-woot-600 text-woot-50': messageType === MESSAGE_TYPE.OUTGOING,
|
||||
'-mx-2': !parentHasAttachments,
|
||||
}"
|
||||
>
|
||||
<message-preview
|
||||
@ -33,6 +34,10 @@ export default {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
parentHasAttachments: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return { MESSAGE_TYPE };
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="video message-text__wrap">
|
||||
<video :src="url" muted playsInline @click="onClick" />
|
||||
<video ref="videoElement" :src="url" muted playsInline @click="onClick" />
|
||||
<woot-modal :show.sync="show" :on-close="onClose">
|
||||
<video
|
||||
:src="url"
|
||||
controls
|
||||
playsInline
|
||||
class="modal-video skip-context-menu"
|
||||
class="modal-video skip-context-menu mx-auto"
|
||||
/>
|
||||
</woot-modal>
|
||||
</div>
|
||||
@ -25,6 +25,11 @@ export default {
|
||||
show: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.videoElement.onerror = () => {
|
||||
this.$emit('error');
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onClose() {
|
||||
this.show = false;
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
<template>
|
||||
<div class="image-placeholder">
|
||||
<fluent-icon icon="document-error" size="32" />
|
||||
<p>{{ $t('COMPONENTS.FILE_BUBBLE.INSTAGRAM_STORY_UNAVAILABLE') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-placeholder {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
padding: var(--space-slab);
|
||||
height: calc(var(--space-large) * 10);
|
||||
margin-top: var(--space-one);
|
||||
width: 100%;
|
||||
background-color: var(--s-75);
|
||||
color: var(--s-800);
|
||||
border-radius: var(--border-radius-normal);
|
||||
}
|
||||
</style>
|
||||
@ -16,5 +16,5 @@ export const FEATURE_FLAGS = {
|
||||
TEAM_MANAGEMENT: 'team_management',
|
||||
VOICE_RECORDER: 'voice_recorder',
|
||||
AUDIT_LOGS: 'audit_logs',
|
||||
MESSAGE_REPLY_TO: 'message_reply_to',
|
||||
INSERT_ARTICLE_IN_REPLY: 'insert_article_in_reply',
|
||||
};
|
||||
|
||||
@ -3,6 +3,7 @@ export const CONVERSATION_EVENTS = Object.freeze({
|
||||
SENT_MESSAGE: 'Sent a message',
|
||||
SENT_PRIVATE_NOTE: 'Sent a private note',
|
||||
INSERTED_A_CANNED_RESPONSE: 'Inserted a canned response',
|
||||
TRANSLATE_A_MESSAGE: 'Translated a message',
|
||||
INSERTED_A_VARIABLE: 'Inserted a variable',
|
||||
USED_MENTIONS: 'Used mentions',
|
||||
SEARCH_CONVERSATION: 'Searched conversations',
|
||||
|
||||
@ -64,3 +64,47 @@ export const isValidURL = value => {
|
||||
/^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/gm;
|
||||
return URL_REGEX.test(value);
|
||||
};
|
||||
|
||||
export const getArticleSearchURL = ({
|
||||
host,
|
||||
portalSlug,
|
||||
pageNumber,
|
||||
locale,
|
||||
status,
|
||||
authorId,
|
||||
categorySlug,
|
||||
sort,
|
||||
query,
|
||||
}) => {
|
||||
const queryParams = new URLSearchParams({});
|
||||
|
||||
const params = {
|
||||
page: pageNumber,
|
||||
locale,
|
||||
status,
|
||||
author_id: authorId,
|
||||
category_slug: categorySlug,
|
||||
sort,
|
||||
query,
|
||||
};
|
||||
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== null && value !== undefined) {
|
||||
queryParams.set(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
return `${host}/${portalSlug}/articles?${queryParams.toString()}`;
|
||||
};
|
||||
|
||||
export const hasValidAvatarUrl = avatarUrl => {
|
||||
try {
|
||||
const { host: avatarUrlHost } = new URL(avatarUrl);
|
||||
const isFromGravatar = ['www.gravatar.com', 'gravatar'].includes(
|
||||
avatarUrlHost
|
||||
);
|
||||
return avatarUrl && !isFromGravatar;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@ -135,6 +135,7 @@ export const getActionOptions = ({ agents, teams, labels, type }) => {
|
||||
assign_team: teams,
|
||||
send_email_to_team: teams,
|
||||
add_label: generateConditionOptions(labels, 'title'),
|
||||
remove_label: generateConditionOptions(labels, 'title'),
|
||||
change_priority: PRIORITY_CONDITION_VALUES,
|
||||
};
|
||||
return actionsMap[type];
|
||||
|
||||
@ -3,6 +3,8 @@ import {
|
||||
conversationUrl,
|
||||
isValidURL,
|
||||
conversationListPageURL,
|
||||
getArticleSearchURL,
|
||||
hasValidAvatarUrl,
|
||||
} from '../URLHelper';
|
||||
|
||||
describe('#URL Helpers', () => {
|
||||
@ -75,4 +77,117 @@ describe('#URL Helpers', () => {
|
||||
expect(isValidURL('alert.window')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getArticleSearchURL', () => {
|
||||
it('should generate a basic URL without optional parameters', () => {
|
||||
const url = getArticleSearchURL({
|
||||
portalSlug: 'news',
|
||||
pageNumber: 1,
|
||||
locale: 'en',
|
||||
host: 'myurl.com',
|
||||
});
|
||||
expect(url).toBe('myurl.com/news/articles?page=1&locale=en');
|
||||
});
|
||||
|
||||
it('should include status parameter if provided', () => {
|
||||
const url = getArticleSearchURL({
|
||||
portalSlug: 'news',
|
||||
pageNumber: 1,
|
||||
locale: 'en',
|
||||
status: 'published',
|
||||
host: 'myurl.com',
|
||||
});
|
||||
expect(url).toBe(
|
||||
'myurl.com/news/articles?page=1&locale=en&status=published'
|
||||
);
|
||||
});
|
||||
|
||||
it('should include author_id parameter if provided', () => {
|
||||
const url = getArticleSearchURL({
|
||||
portalSlug: 'news',
|
||||
pageNumber: 1,
|
||||
locale: 'en',
|
||||
authorId: 123,
|
||||
host: 'myurl.com',
|
||||
});
|
||||
expect(url).toBe(
|
||||
'myurl.com/news/articles?page=1&locale=en&author_id=123'
|
||||
);
|
||||
});
|
||||
|
||||
it('should include category_slug parameter if provided', () => {
|
||||
const url = getArticleSearchURL({
|
||||
portalSlug: 'news',
|
||||
pageNumber: 1,
|
||||
locale: 'en',
|
||||
categorySlug: 'technology',
|
||||
host: 'myurl.com',
|
||||
});
|
||||
expect(url).toBe(
|
||||
'myurl.com/news/articles?page=1&locale=en&category_slug=technology'
|
||||
);
|
||||
});
|
||||
|
||||
it('should include sort parameter if provided', () => {
|
||||
const url = getArticleSearchURL({
|
||||
portalSlug: 'news',
|
||||
pageNumber: 1,
|
||||
locale: 'en',
|
||||
sort: 'views',
|
||||
host: 'myurl.com',
|
||||
});
|
||||
expect(url).toBe('myurl.com/news/articles?page=1&locale=en&sort=views');
|
||||
});
|
||||
|
||||
it('should handle multiple optional parameters', () => {
|
||||
const url = getArticleSearchURL({
|
||||
portalSlug: 'news',
|
||||
pageNumber: 1,
|
||||
locale: 'en',
|
||||
status: 'draft',
|
||||
authorId: 456,
|
||||
categorySlug: 'science',
|
||||
sort: 'views',
|
||||
host: 'myurl.com',
|
||||
});
|
||||
expect(url).toBe(
|
||||
'myurl.com/news/articles?page=1&locale=en&status=draft&author_id=456&category_slug=science&sort=views'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle missing optional parameters gracefully', () => {
|
||||
const url = getArticleSearchURL({
|
||||
portalSlug: 'news',
|
||||
pageNumber: 1,
|
||||
locale: 'en',
|
||||
host: 'myurl.com',
|
||||
});
|
||||
expect(url).toBe('myurl.com/news/articles?page=1&locale=en');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasValidAvatarUrl', () => {
|
||||
test('should return true for valid non-Gravatar URL', () => {
|
||||
expect(hasValidAvatarUrl('https://chatwoot.com/avatar.jpg')).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false for a Gravatar URL (www.gravatar.com)', () => {
|
||||
expect(hasValidAvatarUrl('https://www.gravatar.com/avatar.jpg')).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test('should return false for a Gravatar URL (gravatar)', () => {
|
||||
expect(hasValidAvatarUrl('https://gravatar/avatar.jpg')).toBe(false);
|
||||
});
|
||||
|
||||
test('should handle invalid URL', () => {
|
||||
expect(hasValidAvatarUrl('invalid-url')).toBe(false); // or expect an error, depending on function design
|
||||
});
|
||||
|
||||
test('should return false for empty or undefined URL', () => {
|
||||
expect(hasValidAvatarUrl('')).toBe(false);
|
||||
expect(hasValidAvatarUrl()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"EMOJI": {
|
||||
"PLACEHOLDER": "Search emojis",
|
||||
"NOT_FOUND": "No emoji match your search"
|
||||
"NOT_FOUND": "No emoji match your search",
|
||||
"REMOVE": "Remove"
|
||||
}
|
||||
}
|
||||
|
||||
@ -365,7 +365,7 @@
|
||||
"NAME": {
|
||||
"LABEL": "Name",
|
||||
"PLACEHOLDER": "Category name",
|
||||
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
|
||||
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
|
||||
"ERROR": "Name is required"
|
||||
},
|
||||
"SLUG": {
|
||||
@ -396,7 +396,7 @@
|
||||
"NAME": {
|
||||
"LABEL": "Name",
|
||||
"PLACEHOLDER": "Category name",
|
||||
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
|
||||
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
|
||||
"ERROR": "Name is required"
|
||||
},
|
||||
"SLUG": {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"EMOJI": {
|
||||
"PLACEHOLDER": "ابحث في الايموجي",
|
||||
"NOT_FOUND": "لا يوجد إيموجي يطابق بحثك"
|
||||
"NOT_FOUND": "لا يوجد إيموجي يطابق بحثك",
|
||||
"REMOVE": "حذف"
|
||||
}
|
||||
}
|
||||
|
||||
@ -365,7 +365,7 @@
|
||||
"NAME": {
|
||||
"LABEL": "الاسم",
|
||||
"PLACEHOLDER": "Category name",
|
||||
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
|
||||
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
|
||||
"ERROR": "الاسم مطلوب"
|
||||
},
|
||||
"SLUG": {
|
||||
@ -396,7 +396,7 @@
|
||||
"NAME": {
|
||||
"LABEL": "الاسم",
|
||||
"PLACEHOLDER": "Category name",
|
||||
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
|
||||
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
|
||||
"ERROR": "الاسم مطلوب"
|
||||
},
|
||||
"SLUG": {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"EMOJI": {
|
||||
"PLACEHOLDER": "Search emojis",
|
||||
"NOT_FOUND": "No emoji match your search"
|
||||
"NOT_FOUND": "No emoji match your search",
|
||||
"REMOVE": "Remove"
|
||||
}
|
||||
}
|
||||
|
||||
@ -365,7 +365,7 @@
|
||||
"NAME": {
|
||||
"LABEL": "Име",
|
||||
"PLACEHOLDER": "Category name",
|
||||
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
|
||||
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
|
||||
"ERROR": "Name is required"
|
||||
},
|
||||
"SLUG": {
|
||||
@ -396,7 +396,7 @@
|
||||
"NAME": {
|
||||
"LABEL": "Име",
|
||||
"PLACEHOLDER": "Category name",
|
||||
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
|
||||
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
|
||||
"ERROR": "Name is required"
|
||||
},
|
||||
"SLUG": {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"EMOJI": {
|
||||
"PLACEHOLDER": "Search emojis",
|
||||
"NOT_FOUND": "No emoji match your search"
|
||||
"NOT_FOUND": "No emoji match your search",
|
||||
"REMOVE": "Suprimeix"
|
||||
}
|
||||
}
|
||||
|
||||
@ -365,7 +365,7 @@
|
||||
"NAME": {
|
||||
"LABEL": "Nom",
|
||||
"PLACEHOLDER": "Category name",
|
||||
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
|
||||
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
|
||||
"ERROR": "Name is required"
|
||||
},
|
||||
"SLUG": {
|
||||
@ -396,7 +396,7 @@
|
||||
"NAME": {
|
||||
"LABEL": "Nom",
|
||||
"PLACEHOLDER": "Category name",
|
||||
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
|
||||
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
|
||||
"ERROR": "Name is required"
|
||||
},
|
||||
"SLUG": {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"EMOJI": {
|
||||
"PLACEHOLDER": "Search emojis",
|
||||
"NOT_FOUND": "No emoji match your search"
|
||||
"NOT_FOUND": "No emoji match your search",
|
||||
"REMOVE": "Odebrat"
|
||||
}
|
||||
}
|
||||
|
||||
@ -365,7 +365,7 @@
|
||||
"NAME": {
|
||||
"LABEL": "Název",
|
||||
"PLACEHOLDER": "Category name",
|
||||
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
|
||||
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
|
||||
"ERROR": "Name is required"
|
||||
},
|
||||
"SLUG": {
|
||||
@ -396,7 +396,7 @@
|
||||
"NAME": {
|
||||
"LABEL": "Název",
|
||||
"PLACEHOLDER": "Category name",
|
||||
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
|
||||
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
|
||||
"ERROR": "Name is required"
|
||||
},
|
||||
"SLUG": {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"EMOJI": {
|
||||
"PLACEHOLDER": "Søg emojis",
|
||||
"NOT_FOUND": "Ingen emoji matcher din søgning"
|
||||
"NOT_FOUND": "Ingen emoji matcher din søgning",
|
||||
"REMOVE": "Fjern"
|
||||
}
|
||||
}
|
||||
|
||||
@ -365,7 +365,7 @@
|
||||
"NAME": {
|
||||
"LABEL": "Navn",
|
||||
"PLACEHOLDER": "Kategori navn",
|
||||
"HELP_TEXT": "Kategorinavnet vil blive brugt i den offentlige vendende portal til at kategorisere artikler.",
|
||||
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
|
||||
"ERROR": "Navn er påkrævet"
|
||||
},
|
||||
"SLUG": {
|
||||
@ -396,7 +396,7 @@
|
||||
"NAME": {
|
||||
"LABEL": "Navn",
|
||||
"PLACEHOLDER": "Kategori navn",
|
||||
"HELP_TEXT": "Kategorinavnet vil blive brugt i den offentlige vendende portal til at kategorisere artikler.",
|
||||
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
|
||||
"ERROR": "Navn er påkrævet"
|
||||
},
|
||||
"SLUG": {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"EMOJI": {
|
||||
"PLACEHOLDER": "Emojis durchsuchen",
|
||||
"NOT_FOUND": "Kein Emoji entspricht deiner Suche"
|
||||
"NOT_FOUND": "Kein Emoji entspricht deiner Suche",
|
||||
"REMOVE": "Entfernen"
|
||||
}
|
||||
}
|
||||
|
||||
@ -365,7 +365,7 @@
|
||||
"NAME": {
|
||||
"LABEL": "Name",
|
||||
"PLACEHOLDER": "Kategoriename",
|
||||
"HELP_TEXT": "Der Kategoriename wird im öffentlich zugänglichen Portal verwendet, um Artikel zu kategorisieren.",
|
||||
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
|
||||
"ERROR": "Name wird benötigt"
|
||||
},
|
||||
"SLUG": {
|
||||
@ -396,7 +396,7 @@
|
||||
"NAME": {
|
||||
"LABEL": "Name",
|
||||
"PLACEHOLDER": "Kategoriename",
|
||||
"HELP_TEXT": "Der Kategoriename wird im öffentlich zugänglichen Portal verwendet, um Artikel zu kategorisieren.",
|
||||
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
|
||||
"ERROR": "Name wird benötigt"
|
||||
},
|
||||
"SLUG": {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"EMOJI": {
|
||||
"PLACEHOLDER": "Αναζήτηση emojis",
|
||||
"NOT_FOUND": "Κανένα emoji δεν ταιριάζει με την αναζήτησή σας"
|
||||
"NOT_FOUND": "Κανένα emoji δεν ταιριάζει με την αναζήτησή σας",
|
||||
"REMOVE": "Διαγραφή"
|
||||
}
|
||||
}
|
||||
|
||||
@ -365,7 +365,7 @@
|
||||
"NAME": {
|
||||
"LABEL": "Όνομα",
|
||||
"PLACEHOLDER": "Όνομα κατηγορίας",
|
||||
"HELP_TEXT": "Η κατηγορία θα χρησιμοποιηθεί στην πύλη που βλέπει το κοινό για την κατηγοριοποίηση των άρθρων.",
|
||||
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
|
||||
"ERROR": "Απαιτείται όνομα"
|
||||
},
|
||||
"SLUG": {
|
||||
@ -396,7 +396,7 @@
|
||||
"NAME": {
|
||||
"LABEL": "Όνομα",
|
||||
"PLACEHOLDER": "Όνομα κατηγορίας",
|
||||
"HELP_TEXT": "Η κατηγορία θα χρησιμοποιηθεί στην πύλη που βλέπει το κοινό για την κατηγοριοποίηση των άρθρων.",
|
||||
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
|
||||
"ERROR": "Απαιτείται όνομα"
|
||||
},
|
||||
"SLUG": {
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
{
|
||||
"FILTER": {
|
||||
"TITLE": "Filter Conversations",
|
||||
"SUBTITLE": "Add filters below and hit 'Apply filters' to filter conversations.",
|
||||
"TITLE": "Filter conversations",
|
||||
"SUBTITLE": "Add your filters below and hit 'Apply filters' to cut through the chat clutter.",
|
||||
"EDIT_CUSTOM_FILTER": "Edit Folder",
|
||||
"CUSTOM_VIEWS_SUBTITLE": "Add or remove filters and update your folder.",
|
||||
"ADD_NEW_FILTER": "Add Filter",
|
||||
"FILTER_DELETE_ERROR": "You should have atleast one filter to save",
|
||||
"ADD_NEW_FILTER": "Add filter",
|
||||
"FILTER_DELETE_ERROR": "Oops, looks like we can't save nothing! Please add at least one filter to save it.",
|
||||
"SUBMIT_BUTTON_LABEL": "Apply filters",
|
||||
"UPDATE_BUTTON_LABEL": "Update folder",
|
||||
"CANCEL_BUTTON_LABEL": "Cancel",
|
||||
"CLEAR_BUTTON_LABEL": "Clear Filters",
|
||||
"EMPTY_VALUE_ERROR": "Value is required",
|
||||
"CLEAR_BUTTON_LABEL": "Clear filters",
|
||||
"FOLDER_LABEL": "Folder Name",
|
||||
"FOLDER_QUERY_LABEL": "Folder Query",
|
||||
"EMPTY_VALUE_ERROR": "Value is required.",
|
||||
"TOOLTIP_LABEL": "Filter conversations",
|
||||
"QUERY_DROPDOWN_LABELS": {
|
||||
"AND": "AND",
|
||||
@ -36,44 +36,44 @@
|
||||
},
|
||||
"ATTRIBUTES": {
|
||||
"STATUS": "Status",
|
||||
"ASSIGNEE_NAME": "Assignee Name",
|
||||
"INBOX_NAME": "Inbox Name",
|
||||
"TEAM_NAME": "Team Name",
|
||||
"CONVERSATION_IDENTIFIER": "Conversation Identifier",
|
||||
"CAMPAIGN_NAME": "Campaign Name",
|
||||
"ASSIGNEE_NAME": "Assignee name",
|
||||
"INBOX_NAME": "Inbox name",
|
||||
"TEAM_NAME": "Team name",
|
||||
"CONVERSATION_IDENTIFIER": "Conversation identifier",
|
||||
"CAMPAIGN_NAME": "Campaign name",
|
||||
"LABELS": "Labels",
|
||||
"BROWSER_LANGUAGE": "Browser Language",
|
||||
"BROWSER_LANGUAGE": "Browser language",
|
||||
"PRIORITY": "Priority",
|
||||
"COUNTRY_NAME": "Country Name",
|
||||
"COUNTRY_NAME": "Country name",
|
||||
"REFERER_LINK": "Referer link",
|
||||
"CUSTOM_ATTRIBUTE_LIST": "List",
|
||||
"CUSTOM_ATTRIBUTE_TEXT": "Text",
|
||||
"CUSTOM_ATTRIBUTE_NUMBER": "Number",
|
||||
"CUSTOM_ATTRIBUTE_LINK": "Link",
|
||||
"CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox",
|
||||
"CREATED_AT": "Created At",
|
||||
"LAST_ACTIVITY": "Last Activity"
|
||||
"CREATED_AT": "Created at",
|
||||
"LAST_ACTIVITY": "Last activity"
|
||||
},
|
||||
"GROUPS": {
|
||||
"STANDARD_FILTERS": "Standard Filters",
|
||||
"ADDITIONAL_FILTERS": "Additional Filters",
|
||||
"CUSTOM_ATTRIBUTES": "Custom Attributes"
|
||||
"STANDARD_FILTERS": "Standard filters",
|
||||
"ADDITIONAL_FILTERS": "Additional filters",
|
||||
"CUSTOM_ATTRIBUTES": "Custom attributes"
|
||||
},
|
||||
"CUSTOM_VIEWS": {
|
||||
"ADD": {
|
||||
"TITLE": "Do you want to save this filter?",
|
||||
"LABEL": "Name this filter",
|
||||
"PLACEHOLDER": "Enter a name for this filter",
|
||||
"ERROR_MESSAGE": "Name is required",
|
||||
"PLACEHOLDER": "Name your filter to refer it later.",
|
||||
"ERROR_MESSAGE": "Name is required.",
|
||||
"SAVE_BUTTON": "Save filter",
|
||||
"CANCEL_BUTTON": "Cancel",
|
||||
"API_FOLDERS": {
|
||||
"SUCCESS_MESSAGE": "Folder created successfully",
|
||||
"ERROR_MESSAGE": "Error while creating folder"
|
||||
"SUCCESS_MESSAGE": "Folder created successfully.",
|
||||
"ERROR_MESSAGE": "Error while creating folder."
|
||||
},
|
||||
"API_SEGMENTS": {
|
||||
"SUCCESS_MESSAGE": "Segment created successfully",
|
||||
"ERROR_MESSAGE": "Error while creating segment"
|
||||
"SUCCESS_MESSAGE": "Segment created successfully.",
|
||||
"ERROR_MESSAGE": "Error while creating segment."
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
@ -83,19 +83,19 @@
|
||||
"DELETE_BUTTON": "Delete filter",
|
||||
"MODAL": {
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirm Deletion",
|
||||
"TITLE": "Confirm deletion",
|
||||
"MESSAGE": "Are you sure to delete the filter ",
|
||||
"YES": "Yes, Delete",
|
||||
"NO": "No, Keep it"
|
||||
"YES": "Yes, delete",
|
||||
"NO": "No, keep it"
|
||||
}
|
||||
},
|
||||
"API_FOLDERS": {
|
||||
"SUCCESS_MESSAGE": "Folder deleted successfully",
|
||||
"ERROR_MESSAGE": "Error while deleting folder"
|
||||
"SUCCESS_MESSAGE": "Folder deleted successfully.",
|
||||
"ERROR_MESSAGE": "Error while deleting folder."
|
||||
},
|
||||
"API_SEGMENTS": {
|
||||
"SUCCESS_MESSAGE": "Segment deleted successfully",
|
||||
"ERROR_MESSAGE": "Error while deleting segment"
|
||||
"SUCCESS_MESSAGE": "Segment deleted successfully.",
|
||||
"ERROR_MESSAGE": "Error while deleting segment."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,73 +1,73 @@
|
||||
{
|
||||
"AGENT_BOTS": {
|
||||
"HEADER": "Bots",
|
||||
"LOADING_EDITOR": "Loading Editor...",
|
||||
"HEADER_BTN_TXT": "Add Bot Configuration",
|
||||
"SIDEBAR_TXT": "<p><b>Agent Bots</b> <p>Agent bots allows you to automate the conversations</p>",
|
||||
"LOADING_EDITOR": "Loading editor...",
|
||||
"HEADER_BTN_TXT": "Add bot configuration",
|
||||
"SIDEBAR_TXT": "<p><b>Agent Bots</b> <p>Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try.</p> <p> You can manage your bots from this page or create new ones using the 'Add bot configuraton' button.</p> <p> Open the <a href=\"https://www.chatwoot.com/hc/chatwoot-user-guide-cloud-version/articles/1677497472-how-to-use-agent-bots\" target=\"blank\">Agent bots handbook</a> in another tab for a helping hand.</p>",
|
||||
"CSML_BOT_EDITOR": {
|
||||
"NAME": {
|
||||
"LABEL": "Bot Name",
|
||||
"PLACEHOLDER": "Give your bot a name",
|
||||
"ERROR": "Bot name is required"
|
||||
"LABEL": "Bot name",
|
||||
"PLACEHOLDER": "Name your bot.",
|
||||
"ERROR": "Bot name is required."
|
||||
},
|
||||
"DESCRIPTION": {
|
||||
"LABEL": "Bot Description",
|
||||
"LABEL": "Bot description",
|
||||
"PLACEHOLDER": "What does this bot do?"
|
||||
},
|
||||
"BOT_CONFIG": {
|
||||
"ERROR": "Please enter your CSML bot configuration above",
|
||||
"API_ERROR": "Your CSML configuration is invalid, please fix it and try again."
|
||||
"ERROR": "Please enter your CSML bot configuration above.",
|
||||
"API_ERROR": "Your CSML configuration is invalid. Please fix it and try again."
|
||||
},
|
||||
"SUBMIT": "Validate and save"
|
||||
},
|
||||
"BOT_CONFIGURATION": {
|
||||
"TITLE": "Select an agent bot",
|
||||
"DESC": "You can set an agent bot from the list to this inbox. The bot can initially handle the conversation and transfer it to an agent when needed.",
|
||||
"DESC": "Assign an Agent Bot to your inbox. They can handle initial conversations and transfer them to a live agent when necessary.",
|
||||
"SUBMIT": "Update",
|
||||
"DISCONNECT": "Disconnect Bot",
|
||||
"SUCCESS_MESSAGE": "Successfully updated the agent bot",
|
||||
"DISCONNECTED_SUCCESS_MESSAGE": "Successfully disconnected the agent bot",
|
||||
"ERROR_MESSAGE": "Could not update the agent bot, please try again later",
|
||||
"DISCONNECTED_ERROR_MESSAGE": "Could not disconnect the agent bot, please try again later",
|
||||
"SELECT_PLACEHOLDER": "Select Bot"
|
||||
"DISCONNECT": "Disconnect bot",
|
||||
"SUCCESS_MESSAGE": "Successfully updated the agent bot.",
|
||||
"DISCONNECTED_SUCCESS_MESSAGE": "Successfully disconnected the agent bot.",
|
||||
"ERROR_MESSAGE": "Could not update the agent bot. Please try again.",
|
||||
"DISCONNECTED_ERROR_MESSAGE": "Could not disconnect the agent bot. Please try again.",
|
||||
"SELECT_PLACEHOLDER": "Select bot"
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Configure new bot",
|
||||
"CANCEL_BUTTON_TEXT": "Cancel",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Bot added successfully",
|
||||
"ERROR_MESSAGE": "Could not add bot, Please try again later"
|
||||
"SUCCESS_MESSAGE": "Bot added successfully.",
|
||||
"ERROR_MESSAGE": "Could not add bot. Please try again later."
|
||||
}
|
||||
},
|
||||
"LIST": {
|
||||
"404": "No Bots found, you can create a bot by clicking the 'Configure new bot' Button ↗",
|
||||
"LOADING": "Fetching Bots...",
|
||||
"TYPE": "Bot Type"
|
||||
"404": "No bots found. You can create a bot by clicking the 'Configure new bot' button ↗",
|
||||
"LOADING": "Fetching bots...",
|
||||
"TYPE": "Bot type"
|
||||
},
|
||||
"DELETE": {
|
||||
"BUTTON_TEXT": "Delete",
|
||||
"TITLE": "Delete Bot",
|
||||
"TITLE": "Delete bot",
|
||||
"SUBMIT": "Delete",
|
||||
"CANCEL_BUTTON_TEXT": "Cancel",
|
||||
"DESCRIPTION": "Are you sure you want to delete this bot? This action is irreversible",
|
||||
"DESCRIPTION": "Are you sure you want to delete this bot? This action is irreversible.",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Bot deleted successfully",
|
||||
"ERROR_MESSAGE": "Could not able to delete bot, Please try again later"
|
||||
"SUCCESS_MESSAGE": "Bot deleted successfully.",
|
||||
"ERROR_MESSAGE": "Could not delete bot. Please try again."
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
"BUTTON_TEXT": "Edit",
|
||||
"LOADING": "Fetching Bots...",
|
||||
"TITLE": "Edit Bot",
|
||||
"LOADING": "Fetching bots...",
|
||||
"TITLE": "Edit bot",
|
||||
"CANCEL_BUTTON_TEXT": "Cancel",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Bot updated successfully",
|
||||
"ERROR_MESSAGE": "Could not update bot, Please try again later"
|
||||
"SUCCESS_MESSAGE": "Bot updated successfully.",
|
||||
"ERROR_MESSAGE": "Could not update bot. Please try again."
|
||||
}
|
||||
},
|
||||
"TYPES": {
|
||||
"WEBHOOK": "Webhook Bot",
|
||||
"CSML": "CSML Bot"
|
||||
"WEBHOOK": "Webhook bot",
|
||||
"CSML": "CSML bot"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,40 +1,40 @@
|
||||
{
|
||||
"BULK_ACTION": {
|
||||
"CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected",
|
||||
"AGENT_SELECT_LABEL": "Select Agent",
|
||||
"AGENT_SELECT_LABEL": "Select agent",
|
||||
"ASSIGN_CONFIRMATION_LABEL": "Are you sure to assign %{conversationCount} %{conversationLabel} to",
|
||||
"UNASSIGN_CONFIRMATION_LABEL": "Are you sure to unassign %{conversationCount} %{conversationLabel}?",
|
||||
"GO_BACK_LABEL": "Go back",
|
||||
"ASSIGN_LABEL": "Assign",
|
||||
"YES": "Yes",
|
||||
"ASSIGN_AGENT_TOOLTIP": "Assign Agent",
|
||||
"ASSIGN_AGENT_TOOLTIP": "Assign agent",
|
||||
"ASSIGN_TEAM_TOOLTIP": "Assign team",
|
||||
"ASSIGN_SUCCESFUL": "Conversations assigned successfully",
|
||||
"ASSIGN_FAILED": "Failed to assign conversations, please try again",
|
||||
"RESOLVE_SUCCESFUL": "Conversations resolved successfully",
|
||||
"RESOLVE_FAILED": "Failed to resolve conversations, please try again",
|
||||
"ASSIGN_SUCCESFUL": "Conversations assigned successfully.",
|
||||
"ASSIGN_FAILED": "Failed to assign conversations. Please try again.",
|
||||
"RESOLVE_SUCCESFUL": "Conversations resolved successfully.",
|
||||
"RESOLVE_FAILED": "Failed to resolve conversations. Please try again.",
|
||||
"ALL_CONVERSATIONS_SELECTED_ALERT": "Conversations visible on this page are only selected.",
|
||||
"AGENT_LIST_LOADING": "Loading Agents",
|
||||
"AGENT_LIST_LOADING": "Loading agents",
|
||||
"UPDATE": {
|
||||
"CHANGE_STATUS": "Change status",
|
||||
"SNOOZE_UNTIL_NEXT_REPLY": "Snooze until next reply",
|
||||
"SNOOZE_UNTIL_NEXT_REPLY": "Snooze until next reply.",
|
||||
"UPDATE_SUCCESFUL": "Conversation status updated successfully.",
|
||||
"UPDATE_FAILED": "Failed to update conversations, please try again"
|
||||
"UPDATE_FAILED": "Failed to update conversations. Please try again."
|
||||
},
|
||||
"LABELS": {
|
||||
"ASSIGN_LABELS": "Assign Labels",
|
||||
"ASSIGN_LABELS": "Assign labels",
|
||||
"NO_LABELS_FOUND": "No labels found for",
|
||||
"ASSIGN_SELECTED_LABELS": "Assign selected labels",
|
||||
"ASSIGN_SUCCESFUL": "Labels assigned successfully",
|
||||
"ASSIGN_FAILED": "Failed to assign labels, please try again"
|
||||
"ASSIGN_SUCCESFUL": "Labels assigned successfully.",
|
||||
"ASSIGN_FAILED": "Failed to assign labels. Please try again."
|
||||
},
|
||||
"TEAMS": {
|
||||
"TEAM_SELECT_LABEL": "Select Team",
|
||||
"TEAM_SELECT_LABEL": "Select team",
|
||||
"NONE": "None",
|
||||
"NO_TEAMS_AVAILABLE": "There are no teams added to this account yet.",
|
||||
"ASSIGN_SELECTED_TEAMS": "Assign selected team",
|
||||
"ASSIGN_SUCCESFUL": "Teams assiged successfully",
|
||||
"ASSIGN_FAILED": "Failed to assign team, please try again"
|
||||
"ASSIGN_SELECTED_TEAMS": "Assign selected team.",
|
||||
"ASSIGN_SUCCESFUL": "Teams assiged successfully.",
|
||||
"ASSIGN_FAILED": "Failed to assign team. Please try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,71 +1,71 @@
|
||||
{
|
||||
"CANNED_MGMT": {
|
||||
"HEADER": "Canned Responses",
|
||||
"HEADER_BTN_TXT": "Add Canned Response",
|
||||
"LOADING": "Fetching Canned Responses",
|
||||
"SEARCH_404": "There are no items matching this query",
|
||||
"SIDEBAR_TXT": "<p><b>Canned Responses</b> </p><p> Canned Responses are saved reply templates which can be used to quickly send out a reply to a conversation. </p><p> For creating a Canned Response, just click on the <b>Add Canned Response</b>. You can also edit or delete an existing Canned Response by clicking on the Edit or Delete button </p><p> Canned responses are used with the help of <b>Short Codes</b>. Agents can access canned responses while on a chat by typing <b>'/'</b> followed by the short code. </p>",
|
||||
"HEADER_BTN_TXT": "Add canned response",
|
||||
"LOADING": "Fetching canned responses...",
|
||||
"SEARCH_404": "There are no items matching this query.",
|
||||
"SIDEBAR_TXT": "<p><b>Canned Responses</b> </p><p> Canned Responses are pre-written reply templates that help you quickly respond to a conversation. To insert a canned response during a chat, agents can type a short code preceded by a '/' character. </p><p> You can manage your canned responses from this page or create new ones using the \"Add canned response\" button.</p><p>Open the <a target=\"_blank\" href=\"https://www.chatwoot.com/hc/chatwoot-user-guide-cloud-version/articles/1677501325-how-to-create-saved-reply-templates-with-canned-responses\">Canned Responses handbook</a> in another tab for a helping hand.</p><p>Also, check out the all-new <a href=\"https://www.chatwoot.com/tools/canned-responses-library\" target=\"_blank\">Canned Responses Library</a>.</p>",
|
||||
"LIST": {
|
||||
"404": "There are no canned responses available in this account.",
|
||||
"TITLE": "Manage canned responses",
|
||||
"DESC": "Canned Responses are predefined reply templates which can be used to quickly send out replies to tickets.",
|
||||
"TABLE_HEADER": ["Short Code", "Content", "Actions"]
|
||||
"DESC": "Canned Responses are predefined reply templates which can be used to quickly send out replies to conversations.",
|
||||
"TABLE_HEADER": ["Short code", "Content", "Actions"]
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Add Canned Response",
|
||||
"DESC": "Canned Responses are saved reply templates which can be used to quickly send out reply to conversation.",
|
||||
"TITLE": "Add canned response",
|
||||
"DESC": "Canned Responses are predefined reply templates which can be used to quickly send out replies to conversations.",
|
||||
"CANCEL_BUTTON_TEXT": "Cancel",
|
||||
"FORM": {
|
||||
"SHORT_CODE": {
|
||||
"LABEL": "Short Code",
|
||||
"PLACEHOLDER": "Please enter a short code",
|
||||
"ERROR": "Short Code is required"
|
||||
"LABEL": "Short code",
|
||||
"PLACEHOLDER": "Please enter a short code.",
|
||||
"ERROR": "Short Code is required."
|
||||
},
|
||||
"CONTENT": {
|
||||
"LABEL": "Content",
|
||||
"PLACEHOLDER": "Please enter a content",
|
||||
"ERROR": "Content is required"
|
||||
"LABEL": "Message",
|
||||
"PLACEHOLDER": "Please write the message you want to save as a template to use later.",
|
||||
"ERROR": "Message is required."
|
||||
},
|
||||
"SUBMIT": "Submit"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Canned Response added successfully",
|
||||
"ERROR_MESSAGE": "Could not create canned response. Please try again later."
|
||||
"SUCCESS_MESSAGE": "Canned response added successfully.",
|
||||
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
"TITLE": "Edit Canned Response",
|
||||
"TITLE": "Edit canned response",
|
||||
"CANCEL_BUTTON_TEXT": "Cancel",
|
||||
"FORM": {
|
||||
"SHORT_CODE": {
|
||||
"LABEL": "Short Code",
|
||||
"PLACEHOLDER": "Please enter a shortcode",
|
||||
"ERROR": "Short Code is required"
|
||||
"LABEL": "Short code",
|
||||
"PLACEHOLDER": "Please enter a shortcode.",
|
||||
"ERROR": "Short code is required."
|
||||
},
|
||||
"CONTENT": {
|
||||
"LABEL": "Content",
|
||||
"PLACEHOLDER": "Please enter a content",
|
||||
"ERROR": "Content is required"
|
||||
"LABEL": "Message",
|
||||
"PLACEHOLDER": "Please write the message you want to save as a template to use later.",
|
||||
"ERROR": "Message is required."
|
||||
},
|
||||
"SUBMIT": "Submit"
|
||||
},
|
||||
"BUTTON_TEXT": "Edit",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Canned Response updated successfully",
|
||||
"ERROR_MESSAGE": "Could not update canned response. Please try again later."
|
||||
"SUCCESS_MESSAGE": "Canned response is updated successfully.",
|
||||
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
"BUTTON_TEXT": "Delete",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Canned response deleted successfully",
|
||||
"ERROR_MESSAGE": "Could not delete canned response. Please try again later."
|
||||
"SUCCESS_MESSAGE": "Canned response deleted successfully.",
|
||||
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
|
||||
},
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirm Deletion",
|
||||
"TITLE": "Confirm deletion",
|
||||
"MESSAGE": "Are you sure to delete ",
|
||||
"YES": "Yes, Delete ",
|
||||
"NO": "No, Keep "
|
||||
"YES": "Yes, delete ",
|
||||
"NO": "No, keep "
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -139,6 +139,7 @@
|
||||
"PRIVATE_NOTE": "Private Note",
|
||||
"SEND": "Send",
|
||||
"CREATE": "Add Note",
|
||||
"INSERT_READ_MORE": "Read more",
|
||||
"DISMISS_REPLY": "Dismiss reply",
|
||||
"REPLYING_TO": "Replying to:",
|
||||
"TIP_FORMAT_ICON": "Show rich text editor",
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"EMOJI": {
|
||||
"PLACEHOLDER": "Search emojis",
|
||||
"NOT_FOUND": "No emoji match your search"
|
||||
"NOT_FOUND": "No emoji match your search",
|
||||
"REMOVE": "Remove"
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,6 +76,9 @@
|
||||
},
|
||||
"ARTICLE_SEARCH_RESULT": {
|
||||
"UNCATEGORIZED": "Uncategorized",
|
||||
"SEARCH_RESULTS": "Search results for %{query}",
|
||||
"EMPTY_TEXT": "Search for articles to insert into replies.",
|
||||
"SEARCH_LOADER": "Searching...",
|
||||
"INSERT_ARTICLE": "Insert",
|
||||
"NO_RESULT": "No articles found",
|
||||
"COPY_LINK": "Copy article link to clipboard",
|
||||
@ -220,7 +223,10 @@
|
||||
"LOGO": {
|
||||
"LABEL": "Logo",
|
||||
"UPLOAD_BUTTON": "Upload logo",
|
||||
"HELP_TEXT": "This logo will be displayed on the portal header."
|
||||
"HELP_TEXT": "This logo will be displayed on the portal header.",
|
||||
"IMAGE_UPLOAD_SUCCESS": "Logo uploaded successfully",
|
||||
"IMAGE_UPLOAD_ERROR": "Logo deleted successfully",
|
||||
"IMAGE_DELETE_ERROR": "Error while deleting logo"
|
||||
},
|
||||
"NAME": {
|
||||
"LABEL": "Name",
|
||||
@ -365,7 +371,7 @@
|
||||
"NAME": {
|
||||
"LABEL": "Name",
|
||||
"PLACEHOLDER": "Category name",
|
||||
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
|
||||
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
|
||||
"ERROR": "Name is required"
|
||||
},
|
||||
"SLUG": {
|
||||
@ -396,7 +402,7 @@
|
||||
"NAME": {
|
||||
"LABEL": "Name",
|
||||
"PLACEHOLDER": "Category name",
|
||||
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
|
||||
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
|
||||
"ERROR": "Name is required"
|
||||
},
|
||||
"SLUG": {
|
||||
@ -426,6 +432,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ARTICLE_SEARCH": {
|
||||
"TITLE": "Search articles",
|
||||
"PLACEHOLDER": "Search articles",
|
||||
"NO_RESULT": "No articles found",
|
||||
"SEARCHING": "Searching...",
|
||||
"SEARCH_BUTTON": "Search",
|
||||
"INSERT_ARTICLE": "Insert link",
|
||||
"IFRAME_ERROR": "URL is empty or invalid. Unable to display content.",
|
||||
"OPEN_ARTICLE_SEARCH": "Insert article from Help Center",
|
||||
"SUCCESS_ARTICLE_INSERTED": "Article inserted successfully",
|
||||
"PREVIEW_LINK": "Preview article",
|
||||
"CANCEL": "Close",
|
||||
"BACK": "Back",
|
||||
"BACK_RESULTS": "Back to results"
|
||||
},
|
||||
"UPGRADE_PAGE": {
|
||||
"TITLE": "Help Center",
|
||||
"DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Upgrade your subscription to enable this feature.",
|
||||
|
||||
@ -139,8 +139,8 @@
|
||||
"PRIVATE_NOTE": "Nota privada",
|
||||
"SEND": "Enviar",
|
||||
"CREATE": "Añadir nota",
|
||||
"DISMISS_REPLY": "Dismiss reply",
|
||||
"REPLYING_TO": "Replying to:",
|
||||
"DISMISS_REPLY": "Descartar respuesta",
|
||||
"REPLYING_TO": "Respondiendo a:",
|
||||
"TIP_FORMAT_ICON": "Mostrar editor de textos",
|
||||
"TIP_EMOJI_ICON": "Mostrar selector de emoji",
|
||||
"TIP_ATTACH_ICON": "Adjuntar archivos",
|
||||
@ -195,7 +195,7 @@
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"COPY": "Copiar",
|
||||
"REPLY_TO": "Reply to this message",
|
||||
"REPLY_TO": "Responder a este mensaje",
|
||||
"DELETE": "Eliminar",
|
||||
"CREATE_A_CANNED_RESPONSE": "Añadir a respuestas predefinidas",
|
||||
"TRANSLATE": "Traducir",
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"EMOJI": {
|
||||
"PLACEHOLDER": "Buscar emojis",
|
||||
"NOT_FOUND": "Ningún emoji coincide con tu búsqueda"
|
||||
"NOT_FOUND": "Ningún emoji coincide con tu búsqueda",
|
||||
"REMOVE": "Eliminar"
|
||||
}
|
||||
}
|
||||
|
||||
@ -365,7 +365,7 @@
|
||||
"NAME": {
|
||||
"LABEL": "Nombre",
|
||||
"PLACEHOLDER": "Nombre de categoría",
|
||||
"HELP_TEXT": "El nombre de la categoría se utilizará en el portal público orientado a categorizar los artículos.",
|
||||
"HELP_TEXT": "El nombre de la categoría y el icono se utilizarán en el portal público para categorizar los artículos.",
|
||||
"ERROR": "El nombre es requerido"
|
||||
},
|
||||
"SLUG": {
|
||||
@ -396,7 +396,7 @@
|
||||
"NAME": {
|
||||
"LABEL": "Nombre",
|
||||
"PLACEHOLDER": "Nombre de categoría",
|
||||
"HELP_TEXT": "El nombre de la categoría se utilizará en el portal público orientado a categorizar los artículos.",
|
||||
"HELP_TEXT": "El nombre de la categoría y el icono se utilizarán en el portal público para categorizar los artículos.",
|
||||
"ERROR": "El nombre es requerido"
|
||||
},
|
||||
"SLUG": {
|
||||
@ -428,28 +428,28 @@
|
||||
},
|
||||
"UPGRADE_PAGE": {
|
||||
"TITLE": "Centro de ayuda",
|
||||
"DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Upgrade your subscription to enable this feature.",
|
||||
"SELF_HOSTED_DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Please contact your administrator to enable this feature.",
|
||||
"DESCRIPTION": "Crear portales de autoservicio amigables. Ayuda a tus usuarios a acceder a los artículos y obtener soporte 24/7. Actualiza tu suscripción para activar esta característica.",
|
||||
"SELF_HOSTED_DESCRIPTION": "Crear portales de autoservicio amigables. Ayuda a tus usuarios a acceder a los artículos y obtener soporte 24/7. Póngase en contacto con su administrador para habilitar esta característica.",
|
||||
"BUTTON": {
|
||||
"LEARN_MORE": "Más información",
|
||||
"UPGRADE": "Upgrade"
|
||||
"UPGRADE": "Mejorar"
|
||||
},
|
||||
"FEATURES": {
|
||||
"PORTALS": {
|
||||
"TITLE": "Multiple portals",
|
||||
"DESCRIPTION": "Create multiple help center portals for different products using the same account."
|
||||
"TITLE": "Múltiples portales",
|
||||
"DESCRIPTION": "Crear múltiples portales de centro de ayuda para diferentes productos usando la misma cuenta."
|
||||
},
|
||||
"LOCALES": {
|
||||
"TITLE": "Full support for locales",
|
||||
"DESCRIPTION": "Localize the portal in your language. We support all locales and allow translations for every article."
|
||||
"TITLE": "Soporte completo para locales",
|
||||
"DESCRIPTION": "Localiza el portal en tu idioma. Apoyamos todas las localizaciones y permitimos traducciones para cada artículo."
|
||||
},
|
||||
"SEO": {
|
||||
"TITLE": "SEO-friendly design",
|
||||
"DESCRIPTION": "Customize your meta tags to improve your visibility on search engines with our SEO-friendly pages."
|
||||
"TITLE": "Diseño amigable con SEO",
|
||||
"DESCRIPTION": "Personalice sus meta-etiquetas para mejorar su visibilidad en los motores de búsqueda con nuestras páginas amigables con SEO."
|
||||
},
|
||||
"API": {
|
||||
"TITLE": "Full API support",
|
||||
"DESCRIPTION": "Use the portal as a headless CMS with third party front-end frameworks using our APIs."
|
||||
"TITLE": "Soporte completo de API",
|
||||
"DESCRIPTION": "Utilice el portal como un CMS sin cabeceras con frameworks front-end de terceros usando nuestras APIs."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,7 +39,7 @@
|
||||
"SORT_TOOLTIP_LABEL": "مرتبسازی گفتگوها",
|
||||
"CHAT_SORT": {
|
||||
"STATUS": "وضعیت",
|
||||
"ORDER_BY": "سفارش توسط"
|
||||
"ORDER_BY": "مرتب بر اساس"
|
||||
},
|
||||
"CHAT_TIME_STAMP": {
|
||||
"CREATED": {
|
||||
@ -62,7 +62,7 @@
|
||||
"TEXT": "اولویت"
|
||||
},
|
||||
"sort_on_waiting_since": {
|
||||
"TEXT": "پاسخ در انتظار"
|
||||
"TEXT": "در انتظار پاسخ"
|
||||
}
|
||||
},
|
||||
"ATTACHMENTS": {
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
"LOCATION": "مکان",
|
||||
"BROWSER_LANGUAGE": "مرور زبان",
|
||||
"CONVERSATION_TITLE": "جزئیات مکالمه",
|
||||
"VIEW_PROFILE": "نمایش مشخصات",
|
||||
"VIEW_PROFILE": "نمایش پروفایل",
|
||||
"BROWSER": "مرورگر",
|
||||
"OS": "سیستم عامل",
|
||||
"INITIATED_FROM": "شروع شده از",
|
||||
@ -62,13 +62,13 @@
|
||||
"DESC": "اطلاعات اولیه درباره مخاطب را اضافه کنید."
|
||||
},
|
||||
"IMPORT_CONTACTS": {
|
||||
"BUTTON_LABEL": "وارد كردن",
|
||||
"TITLE": "وارد کردن مخاطبین",
|
||||
"BUTTON_LABEL": "تزریق کردن",
|
||||
"TITLE": "تزریق مخاطبین",
|
||||
"DESC": "مخاطبین را از طریق یک فایل CSV وارد کنید.",
|
||||
"DOWNLOAD_LABEL": "نمونه csv را بارگیری کنید.",
|
||||
"FORM": {
|
||||
"LABEL": "پرونده CSV",
|
||||
"SUBMIT": "وارد كردن",
|
||||
"SUBMIT": "تزریق کردن",
|
||||
"CANCEL": "انصراف"
|
||||
},
|
||||
"SUCCESS_MESSAGE": "You will be notified via email when the import is complete.",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"CONVERSATION": {
|
||||
"SELECT_A_CONVERSATION": "لطفا یک گفتگو را از پنجره گفتگوها انتخاب کنید",
|
||||
"CSAT_REPLY_MESSAGE": "لطفاً به مکالمه امتیاز دهید",
|
||||
"CSAT_REPLY_MESSAGE": "لطفاً به گفتگو امتیاز دهید",
|
||||
"404": "با عرض پوزش، ما نمیتوانیم گفتگو را پیدا کنیم. لطفا دوباره تلاش کنید",
|
||||
"SWITCH_VIEW_LAYOUT": "تغییر طرحبندی",
|
||||
"DASHBOARD_APP_TAB_MESSAGES": "پیامها",
|
||||
@ -37,7 +37,7 @@
|
||||
"REPLYING_TO": "شما در حال پاسخ دادن به:",
|
||||
"REMOVE_SELECTION": "حذف انتخابشدهها",
|
||||
"DOWNLOAD": "دانلود",
|
||||
"UNKNOWN_FILE_TYPE": "پرونده ناشناخته",
|
||||
"UNKNOWN_FILE_TYPE": "فایل ناشناخته",
|
||||
"SAVE_CONTACT": "ذخیره",
|
||||
"UPLOADING_ATTACHMENTS": "در حال بارگذاری پیوستها...",
|
||||
"REPLIED_TO_STORY": "Replied to your story",
|
||||
@ -46,7 +46,7 @@
|
||||
"NO_RESPONSE": "بدون پاسخ",
|
||||
"RATING_TITLE": "رتبه",
|
||||
"FEEDBACK_TITLE": "بازخورد",
|
||||
"REPLY_MESSAGE_NOT_FOUND": "Message not available",
|
||||
"REPLY_MESSAGE_NOT_FOUND": "پیام در دسترس نیست",
|
||||
"CARD": {
|
||||
"SHOW_LABELS": "مشاهده کردن برچسبها",
|
||||
"HIDE_LABELS": "پنهان کردن برچسبها"
|
||||
@ -58,7 +58,7 @@
|
||||
"OPEN": "بیشتر",
|
||||
"CLOSE": "بستن",
|
||||
"DETAILS": "جزئیات",
|
||||
"SNOOZED_UNTIL": "Snoozed until",
|
||||
"SNOOZED_UNTIL": "به تعویق افتاده تا",
|
||||
"SNOOZED_UNTIL_TOMORROW": "تا فردا به تعویق افتاد",
|
||||
"SNOOZED_UNTIL_NEXT_WEEK": "تا هفته آینده به تعویق افتاد",
|
||||
"SNOOZED_UNTIL_NEXT_REPLY": "تا پاسخ بعدی به تعویق افتاد"
|
||||
@ -76,23 +76,23 @@
|
||||
"CUSTOM_SNOOZE": {
|
||||
"TITLE": "به تعویق انداختن تا",
|
||||
"APPLY": "به تعویق انداختن",
|
||||
"CANCEL": "انصراف"
|
||||
"CANCEL": "لغو"
|
||||
},
|
||||
"PRIORITY": {
|
||||
"TITLE": "اولویت",
|
||||
"OPTIONS": {
|
||||
"NONE": "هیچکدام",
|
||||
"URGENT": "Urgent",
|
||||
"HIGH": "High",
|
||||
"MEDIUM": "Medium",
|
||||
"LOW": "Low"
|
||||
"HIGH": "بالا",
|
||||
"MEDIUM": "متوسط",
|
||||
"LOW": "پایین"
|
||||
},
|
||||
"CHANGE_PRIORITY": {
|
||||
"SELECT_PLACEHOLDER": "هیچکدام",
|
||||
"INPUT_PLACEHOLDER": "Select priority",
|
||||
"INPUT_PLACEHOLDER": "انتخاب اولویت",
|
||||
"NO_RESULTS": "نتیجهای یافت نشد",
|
||||
"SUCCESSFUL": "Changed priority of conversation id %{conversationId} to %{priority}",
|
||||
"FAILED": "Couldn't change priority. Please try again."
|
||||
"SUCCESSFUL": "تغییر اولویت گفتگوی با شناسه %{conversationId} به %{priority}",
|
||||
"FAILED": "تغییر اولویت گفتگو با موفقیت انجام نشد. لطفا دوباره تلاش نمایید."
|
||||
}
|
||||
},
|
||||
"CARD_CONTEXT_MENU": {
|
||||
@ -139,8 +139,8 @@
|
||||
"PRIVATE_NOTE": "یادداشت خصوصی",
|
||||
"SEND": "ارسال",
|
||||
"CREATE": "افزودن یادداشت",
|
||||
"DISMISS_REPLY": "Dismiss reply",
|
||||
"REPLYING_TO": "Replying to:",
|
||||
"DISMISS_REPLY": "رد پاسخ",
|
||||
"REPLYING_TO": "در حال پاسخ دهی به:",
|
||||
"TIP_FORMAT_ICON": "نمایش ویرایشگر متنی پیشرفته",
|
||||
"TIP_EMOJI_ICON": "انتخاب ایموجی",
|
||||
"TIP_ATTACH_ICON": "ضمیمه فایل",
|
||||
@ -152,10 +152,10 @@
|
||||
"STOP_AUDIO_RECORDING": "در حال توقف ضبط صدا",
|
||||
"": "",
|
||||
"EMAIL_HEAD": {
|
||||
"TO": "TO",
|
||||
"ADD_BCC": "افزودن bcc",
|
||||
"TO": "به",
|
||||
"ADD_BCC": "افزودن رونوشت",
|
||||
"CC": {
|
||||
"LABEL": "CC",
|
||||
"LABEL": "رونوشت",
|
||||
"PLACEHOLDER": "ایمیلها با کاما از هم جدا میشوند",
|
||||
"ERROR": "لطفا آدرس ایمیل معتبر وارد کنید"
|
||||
},
|
||||
@ -195,7 +195,7 @@
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"COPY": "کپی",
|
||||
"REPLY_TO": "Reply to this message",
|
||||
"REPLY_TO": "پاسخ به این پیام",
|
||||
"DELETE": "حذف",
|
||||
"CREATE_A_CANNED_RESPONSE": "اضافه کردن به پاسخهای آماده",
|
||||
"TRANSLATE": "ترجمه",
|
||||
@ -296,7 +296,7 @@
|
||||
"SUBJECT": "موضوع"
|
||||
},
|
||||
"CONVERSATION_PARTICIPANTS": {
|
||||
"SIDEBAR_MENU_TITLE": "شرکت کننده",
|
||||
"SIDEBAR_MENU_TITLE": "شرکت کنندگان",
|
||||
"SIDEBAR_TITLE": "شرکت کنندگان در گفتگو",
|
||||
"NO_RECORDS_FOUND": "نتیجهای یافت نشد",
|
||||
"ADD_PARTICIPANTS": "شرکت کنندگان را انتخاب کنید",
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"EMOJI": {
|
||||
"PLACEHOLDER": "جستجوی ایموجی",
|
||||
"NOT_FOUND": "هیچ ایموجی با جستجوی شما مطابقت ندارد"
|
||||
"NOT_FOUND": "هیچ ایموجی با جستجوی شما مطابقت ندارد",
|
||||
"REMOVE": "حذف"
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"TITLE": "تنظیمات حساب",
|
||||
"SUBMIT": "بهروزرسانی تنظیمات",
|
||||
"BACK": "بازگشت",
|
||||
"DISMISS": "Dismiss",
|
||||
"DISMISS": "نادیده بگیر",
|
||||
"UPDATE": {
|
||||
"ERROR": "تنظیمات بهروزرسانی نشد، دوباره امتحان کنید!",
|
||||
"SUCCESS": "تنظیمات با موفقیت اعمال شد"
|
||||
@ -92,7 +92,7 @@
|
||||
},
|
||||
"NETWORK": {
|
||||
"NOTIFICATION": {
|
||||
"TEXT": "قطع کردن Chatwoot"
|
||||
"TEXT": "ارتباط با سیستم چت قطع شده است"
|
||||
},
|
||||
"BUTTON": {
|
||||
"REFRESH": "تازه کردن"
|
||||
@ -112,7 +112,7 @@
|
||||
"REMOVE_LABEL": "برچسب را از مکالمه حذف کنید",
|
||||
"SETTINGS": "تنظیمات",
|
||||
"AI_ASSIST": "AI Assist",
|
||||
"APPEARANCE": "Appearance"
|
||||
"APPEARANCE": "ظاهری"
|
||||
},
|
||||
"COMMANDS": {
|
||||
"GO_TO_CONVERSATION_DASHBOARD": "به داشبورد مکالمه بروید",
|
||||
@ -135,7 +135,7 @@
|
||||
"ADD_LABELS_TO_CONVERSATION": "برچسب را به مکالمه اضافه کنید",
|
||||
"ASSIGN_AN_AGENT": "یک ایجنت تعیین کنید",
|
||||
"AI_ASSIST": "AI Assist",
|
||||
"ASSIGN_PRIORITY": "Assign priority",
|
||||
"ASSIGN_PRIORITY": "انتساب اولویت",
|
||||
"ASSIGN_A_TEAM": "یک تیم را تعیین کنید",
|
||||
"MUTE_CONVERSATION": "صدای گفتگو را قطع کن",
|
||||
"UNMUTE_CONVERSATION": "مکالمه را باصدا کنید",
|
||||
@ -147,8 +147,8 @@
|
||||
"UNTIL_NEXT_REPLY": "تا پاسخ بعدی",
|
||||
"UNTIL_NEXT_WEEK": "تا هفته بعد",
|
||||
"UNTIL_TOMORROW": "تا فردا",
|
||||
"UNTIL_NEXT_MONTH": "Until next month",
|
||||
"AN_HOUR_FROM_NOW": "Until an hour from now",
|
||||
"UNTIL_NEXT_MONTH": "تا ماه آینده",
|
||||
"AN_HOUR_FROM_NOW": "از حالا تا یک ساعت دیگر",
|
||||
"CUSTOM": "سفارشی...",
|
||||
"CHANGE_APPEARANCE": "تغییر ظاهر",
|
||||
"LIGHT_MODE": "روشن",
|
||||
|
||||
@ -365,7 +365,7 @@
|
||||
"NAME": {
|
||||
"LABEL": "نام",
|
||||
"PLACEHOLDER": "نام دستهبندی",
|
||||
"HELP_TEXT": "از نام دسته در پرتال عمومی برای دسته بندی مقالات استفاده می شود.",
|
||||
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
|
||||
"ERROR": "نام الزامی است"
|
||||
},
|
||||
"SLUG": {
|
||||
@ -396,7 +396,7 @@
|
||||
"NAME": {
|
||||
"LABEL": "نام",
|
||||
"PLACEHOLDER": "نام دستهبندی",
|
||||
"HELP_TEXT": "از نام دسته در پرتال عمومی برای دسته بندی مقالات استفاده می شود.",
|
||||
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
|
||||
"ERROR": "نام الزامی است"
|
||||
},
|
||||
"SLUG": {
|
||||
|
||||
@ -162,7 +162,7 @@
|
||||
"PROVIDERS": {
|
||||
"LABEL": "ارائه دهنده API",
|
||||
"TWILIO": "Twilio",
|
||||
"BANDWIDTH": "Bandwidth"
|
||||
"BANDWIDTH": "پهنای باند"
|
||||
},
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "ما نتوانستیم کانال پیامک را ذخیره کنیم"
|
||||
@ -405,21 +405,21 @@
|
||||
"DISABLED": "غیرفعال"
|
||||
},
|
||||
"SENDER_NAME_SECTION": {
|
||||
"TITLE": "Sender name",
|
||||
"TITLE": "نام ارسال کننده",
|
||||
"SUB_TEXT": "Select the name shown to the your customer when they receive emails from your agents.",
|
||||
"FOR_EG": "For eg:",
|
||||
"FRIENDLY": {
|
||||
"TITLE": "Friendly",
|
||||
"TITLE": "دوستانه",
|
||||
"FROM": "از",
|
||||
"SUBTITLE": "Add the name of the agent who sent the reply in the sender name to make it friendly."
|
||||
},
|
||||
"PROFESSIONAL": {
|
||||
"TITLE": "Professional",
|
||||
"TITLE": "حرفه ای",
|
||||
"SUBTITLE": "Use only the configured business name as the sender name in the email header."
|
||||
},
|
||||
"BUSINESS_NAME": {
|
||||
"BUTTON_TEXT": "+ Configure your business name",
|
||||
"PLACEHOLDER": "Enter your business name",
|
||||
"PLACEHOLDER": "نام کسب و کار خود را وارد کنید",
|
||||
"SAVE_BUTTON_TEXT": "ذخیره"
|
||||
}
|
||||
},
|
||||
@ -486,7 +486,7 @@
|
||||
"ENABLE_EMAIL_COLLECT_BOX_SUB_TEXT": "فعال یا غیرفعال کردن فرم دریافت ایمیل از کاربر",
|
||||
"AUTO_ASSIGNMENT": "فعال کردن واگذاری خودکار گفتگو به ایجنت ها",
|
||||
"ENABLE_CSAT": "فعال کردن رضایت مشتری",
|
||||
"SENDER_NAME_SECTION": "Enable Agent Name in Email",
|
||||
"SENDER_NAME_SECTION": "فعال سازی نام اپراتور در ایمیل",
|
||||
"ENABLE_CSAT_SUB_TEXT": "پس از پایان گفتگو ، نظرسنجی CSAT (رضایت مشتری) را فعال/غیرفعال کنید",
|
||||
"SENDER_NAME_SECTION_TEXT": "Enable/Disable showing Agent's name in email, if disabled it will show business name",
|
||||
"ENABLE_CONTINUITY_VIA_EMAIL": "ادامه مکالمه را از طریق ایمیل فعال کنید",
|
||||
@ -498,7 +498,7 @@
|
||||
"AUTO_ASSIGNMENT_SUB_TEXT": "فعال کردن یا غیرفعال کردن واگذاری خودکار گفتگوها به ایجنت های عضو این صندوق ورودی.",
|
||||
"HMAC_VERIFICATION": "اعتبار سنجی کاربر",
|
||||
"HMAC_DESCRIPTION": "به منظور اعتبارسنجی هویت کاربر، میتوانید یک «شناسه_هش» برای هر کاربر ارسال کنید. میتوانید با استفاده از «شناسه» با کلید نشان داده شده در اینجا یک هش HMAC sha256 ایجاد کنید.",
|
||||
"HMAC_LINK_TO_DOCS": "You can read more here.",
|
||||
"HMAC_LINK_TO_DOCS": "از اینجا می توانید بیشتر بخوانید.",
|
||||
"HMAC_MANDATORY_VERIFICATION": "مجبور کردن تأیید اعتبار هویت کاربر",
|
||||
"HMAC_MANDATORY_DESCRIPTION": "اگر فعال باشد، درخواستهایی که «identifier_hash» را ندارند رد میشوند.",
|
||||
"INBOX_IDENTIFIER": "شناسه صندوق ورودی",
|
||||
@ -674,7 +674,7 @@
|
||||
"PLACE_HOLDER": "دسترسی به ما ساده است. هر سوالی پیش آمد همینجا از ما بپرسید."
|
||||
},
|
||||
"REPLY_TIME": {
|
||||
"LABEL": "زمان پاسخ",
|
||||
"LABEL": "زمان پاسخ دهی",
|
||||
"IN_A_FEW_MINUTES": "در چند دقیقه",
|
||||
"IN_A_FEW_HOURS": "در چند ساعت",
|
||||
"IN_A_DAY": "در یک روز"
|
||||
@ -712,12 +712,12 @@
|
||||
"CHAT": "گفتگو"
|
||||
},
|
||||
"REPLY_TIME": {
|
||||
"IN_A_FEW_MINUTES": "معمولاً در عرض چند دقیقه پاسخ می دهند",
|
||||
"IN_A_FEW_MINUTES": "معمولاً در عرض چند دقیقه پاسخ دهی انجام می شود",
|
||||
"IN_A_FEW_HOURS": "معمولاً در عرض چند ساعت پاسخ می دهند",
|
||||
"IN_A_DAY": "به طور معمول در یک روز پاسخ می دهند"
|
||||
"IN_A_DAY": "به طور معمول در یک روز پاسخ دهی انجام می شود"
|
||||
},
|
||||
"FOOTER": {
|
||||
"START_CONVERSATION_BUTTON_TEXT": "شروع گفتگو",
|
||||
"START_CONVERSATION_BUTTON_TEXT": "گفتگو را شروع کنید",
|
||||
"CHAT_INPUT_PLACEHOLDER": "پیام خود را وارد کنید"
|
||||
},
|
||||
"BODY": {
|
||||
|
||||
@ -131,7 +131,7 @@
|
||||
"KEY_PLACEHOLDER": "Enter your OpenAI API key",
|
||||
"BUTTONS": {
|
||||
"NEED_HELP": "به کمک احتياج داری؟",
|
||||
"DISMISS": "Dismiss",
|
||||
"DISMISS": "نادیده بگیر",
|
||||
"FINISH": "Finish Setup"
|
||||
},
|
||||
"DISMISS_MESSAGE": "You can setup OpenAI integration later Whenever you want.",
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
"DISMISS": "Dismiss suggestion"
|
||||
},
|
||||
"POWERED_BY": "Chatwoot AI",
|
||||
"DISMISS": "Dismiss",
|
||||
"DISMISS": "نادیده بگیر",
|
||||
"ADD_SELECTED_LABELS": "Add selected labels",
|
||||
"ADD_SELECTED_LABEL": "Add selected label",
|
||||
"ADD_ALL_LABELS": "Add all labels"
|
||||
|
||||
@ -34,7 +34,7 @@
|
||||
"ERROR": "Inbox is required"
|
||||
},
|
||||
"MESSAGE": {
|
||||
"LABEL": "Message",
|
||||
"LABEL": "Viesti",
|
||||
"PLACEHOLDER": "Please enter the message of campaign",
|
||||
"ERROR": "Message is required"
|
||||
},
|
||||
@ -88,7 +88,7 @@
|
||||
"404": "There are no campaigns created for this inbox.",
|
||||
"TABLE_HEADER": {
|
||||
"TITLE": "Title",
|
||||
"MESSAGE": "Message",
|
||||
"MESSAGE": "Viesti",
|
||||
"INBOX": "Inbox",
|
||||
"STATUS": "Tila",
|
||||
"SENDER": "Sender",
|
||||
|
||||
@ -1,75 +1,75 @@
|
||||
{
|
||||
"CANNED_MGMT": {
|
||||
"HEADER": "Tallennetut vastaukset",
|
||||
"HEADER_BTN_TXT": "Lisää tallennettu vastaus",
|
||||
"LOADING": "Haetaan tallennettuja vastauksia",
|
||||
"SEARCH_404": "Tätä hakua vastaavia kohteita ei löydy",
|
||||
"SIDEBAR_TXT": "<p><b>Canned Responses</b> </p><p> Canned Responses are saved reply templates which can be used to quickly send out a reply to a conversation. </p><p> For creating a Canned Response, just click on the <b>Add Canned Response</b>. You can also edit or delete an existing Canned Response by clicking on the Edit or Delete button </p><p> Canned responses are used with the help of <b>Short Codes</b>. Agents can access canned responses while on a chat by typing <b>'/'</b> followed by the short code. </p>",
|
||||
"HEADER_BTN_TXT": "Add canned response",
|
||||
"LOADING": "Fetching canned responses...",
|
||||
"SEARCH_404": "Tätä hakua vastaavia kohteita ei löydy.",
|
||||
"SIDEBAR_TXT": "<p><b>Canned Responses</b> </p><p> Canned Responses are pre-written reply templates that help you quickly respond to a conversation. To insert a canned response during a chat, agents can type a short code preceded by a '/' character. </p><p> You can manage your canned responses from this page or create new ones using the \"Add canned response\" button.</p><p>Open the <a target=\"_blank\" href=\"https://www.chatwoot.com/hc/chatwoot-user-guide-cloud-version/articles/1677501325-how-to-create-saved-reply-templates-with-canned-responses\">Canned Responses handbook</a> in another tab for a helping hand.</p><p>Also, check out the all-new <a href=\"https://www.chatwoot.com/tools/canned-responses-library\" target=\"_blank\">Canned Responses Library</a>.</p>",
|
||||
"LIST": {
|
||||
"404": "Tällä tilillä ei ole tallennettuja vastauksia.",
|
||||
"TITLE": "Hallitse tallennettuja vastauksia",
|
||||
"DESC": "Tallennetut vastaukset ovat ennalta valmisteltuja vastausmalleja, joita voidaan käyttää nopeasti lähettää vastaukset tukipyyntöihin.",
|
||||
"DESC": "Canned Responses are predefined reply templates which can be used to quickly send out replies to conversations.",
|
||||
"TABLE_HEADER": [
|
||||
"Oikotie",
|
||||
"Short code",
|
||||
"Sisältö",
|
||||
"Toiminnot"
|
||||
]
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Lisää tallennettu vastaus",
|
||||
"DESC": "Canned Responses are saved reply templates which can be used to quickly send out reply to conversation.",
|
||||
"TITLE": "Add canned response",
|
||||
"DESC": "Canned Responses are predefined reply templates which can be used to quickly send out replies to conversations.",
|
||||
"CANCEL_BUTTON_TEXT": "Peruuta",
|
||||
"FORM": {
|
||||
"SHORT_CODE": {
|
||||
"LABEL": "Oikotie",
|
||||
"PLACEHOLDER": "Please enter a short code",
|
||||
"ERROR": "Oikotie on vaadittu"
|
||||
"LABEL": "Short code",
|
||||
"PLACEHOLDER": "Please enter a short code.",
|
||||
"ERROR": "Short Code is required."
|
||||
},
|
||||
"CONTENT": {
|
||||
"LABEL": "Sisältö",
|
||||
"PLACEHOLDER": "Ole hyvä ja syötä sisältö",
|
||||
"ERROR": "Sisältö on vaadittu"
|
||||
"LABEL": "Viesti",
|
||||
"PLACEHOLDER": "Please write the message you want to save as a template to use later.",
|
||||
"ERROR": "Message is required."
|
||||
},
|
||||
"SUBMIT": "Lähetä"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Tallennettu vastaus lisätty onnistuneesti",
|
||||
"SUCCESS_MESSAGE": "Canned response added successfully.",
|
||||
"ERROR_MESSAGE": "Yhteyden muodostaminen Woot-palvelimelle ei onnistunut, yritä myöhemmin uudelleen"
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
"TITLE": "Muokkaa tallennettua vastausta",
|
||||
"TITLE": "Edit canned response",
|
||||
"CANCEL_BUTTON_TEXT": "Peruuta",
|
||||
"FORM": {
|
||||
"SHORT_CODE": {
|
||||
"LABEL": "Oikotie",
|
||||
"PLACEHOLDER": "Ole hyvä ja syötä oikotie",
|
||||
"ERROR": "Oikotie on vaadittu"
|
||||
"LABEL": "Short code",
|
||||
"PLACEHOLDER": "Please enter a shortcode.",
|
||||
"ERROR": "Short code is required."
|
||||
},
|
||||
"CONTENT": {
|
||||
"LABEL": "Sisältö",
|
||||
"PLACEHOLDER": "Ole hyvä ja syötä sisältö",
|
||||
"ERROR": "Sisältö on vaadittu"
|
||||
"LABEL": "Viesti",
|
||||
"PLACEHOLDER": "Please write the message you want to save as a template to use later.",
|
||||
"ERROR": "Message is required."
|
||||
},
|
||||
"SUBMIT": "Lähetä"
|
||||
},
|
||||
"BUTTON_TEXT": "Muokkaa",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Tallennettu vastaus päivitetty onnistuneesti",
|
||||
"SUCCESS_MESSAGE": "Canned response is updated successfully.",
|
||||
"ERROR_MESSAGE": "Yhteyden muodostaminen Woot-palvelimelle ei onnistunut, yritä myöhemmin uudelleen"
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
"BUTTON_TEXT": "Poista",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Tallennettu vastaus poistettu onnistuneesti",
|
||||
"SUCCESS_MESSAGE": "Canned response deleted successfully.",
|
||||
"ERROR_MESSAGE": "Yhteyden muodostaminen Woot-palvelimelle ei onnistunut, yritä myöhemmin uudelleen"
|
||||
},
|
||||
"CONFIRM": {
|
||||
"TITLE": "Vahvista poistaminen",
|
||||
"TITLE": "Confirm deletion",
|
||||
"MESSAGE": "Oletko varma että haluat poistaa ",
|
||||
"YES": "Kyllä, poista ",
|
||||
"NO": "Ei, säilytä "
|
||||
"YES": "Yes, delete ",
|
||||
"NO": "No, keep "
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -200,7 +200,7 @@
|
||||
"ERROR": "Subject can't be empty"
|
||||
},
|
||||
"MESSAGE": {
|
||||
"LABEL": "Message",
|
||||
"LABEL": "Viesti",
|
||||
"PLACEHOLDER": "Write your message here",
|
||||
"ERROR": "Message can't be empty"
|
||||
},
|
||||
@ -309,20 +309,20 @@
|
||||
},
|
||||
"VALUE": {
|
||||
"LABEL": "Attribute value",
|
||||
"PLACEHOLDER": "Eg: 11901 "
|
||||
"PLACEHOLDER": "Esim: 11901 "
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Luo uusi määrite ",
|
||||
"SUCCESS": "Attribute added successfully",
|
||||
"ERROR": "Unable to add attribute. Please try again later"
|
||||
"SUCCESS": "Attribuutin lisäys onnistui",
|
||||
"ERROR": "Attribuuttia ei voitu lisätä. Yritä myöhemmin uudelleen"
|
||||
},
|
||||
"UPDATE": {
|
||||
"SUCCESS": "Attribute updated successfully",
|
||||
"ERROR": "Unable to update attribute. Please try again later"
|
||||
"SUCCESS": "Tunniste päivitetty onnistuneesti",
|
||||
"ERROR": "Attribuuttia ei voida päivittää. Yritä myöhemmin uudelleen"
|
||||
},
|
||||
"DELETE": {
|
||||
"SUCCESS": "Attribute deleted successfully",
|
||||
"ERROR": "Unable to delete attribute. Please try again later"
|
||||
"SUCCESS": "Attribuutin poisto onnistui",
|
||||
"ERROR": "Attribuuttia ei voida poistaa. Yritä uudelleen myöhemmin"
|
||||
},
|
||||
"ATTRIBUTE_SELECT": {
|
||||
"TITLE": "Lisää määritteitä",
|
||||
@ -331,15 +331,15 @@
|
||||
},
|
||||
"ATTRIBUTE_TYPE": {
|
||||
"LIST": {
|
||||
"PLACEHOLDER": "Select value",
|
||||
"SEARCH_INPUT_PLACEHOLDER": "Search value",
|
||||
"PLACEHOLDER": "Valitse arvo",
|
||||
"SEARCH_INPUT_PLACEHOLDER": "Etsi arvo",
|
||||
"NO_RESULT": "Tuloksia ei löytynyt"
|
||||
}
|
||||
}
|
||||
},
|
||||
"VALIDATIONS": {
|
||||
"REQUIRED": "Valid value is required",
|
||||
"INVALID_URL": "Invalid URL"
|
||||
"REQUIRED": "Kelvollinen arvo vaaditaan",
|
||||
"INVALID_URL": "Virheellinen URL"
|
||||
}
|
||||
},
|
||||
"MERGE_CONTACTS": {
|
||||
@ -355,7 +355,7 @@
|
||||
"HELP_LABEL": "Säilytetään"
|
||||
},
|
||||
"SUMMARY": {
|
||||
"TITLE": "Summary",
|
||||
"TITLE": "Yhteenveto",
|
||||
"DELETE_WARNING": "Contact of <strong>%{primaryContactName}</strong> will be deleted.",
|
||||
"ATTRIBUTE_WARNING": "Contact details of <strong>%{primaryContactName}</strong> will be copied to <strong>%{parentContactName}</strong>."
|
||||
},
|
||||
|
||||
@ -270,17 +270,17 @@
|
||||
"CONVERSATION_CUSTOM_ATTRIBUTES": {
|
||||
"ADD_BUTTON_TEXT": "Create attribute",
|
||||
"UPDATE": {
|
||||
"SUCCESS": "Attribute updated successfully",
|
||||
"ERROR": "Unable to update attribute. Please try again later"
|
||||
"SUCCESS": "Tunniste päivitetty onnistuneesti",
|
||||
"ERROR": "Attribuuttia ei voida päivittää. Yritä myöhemmin uudelleen"
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Add",
|
||||
"SUCCESS": "Attribute added successfully",
|
||||
"ERROR": "Unable to add attribute. Please try again later"
|
||||
"SUCCESS": "Attribuutin lisäys onnistui",
|
||||
"ERROR": "Attribuuttia ei voitu lisätä. Yritä myöhemmin uudelleen"
|
||||
},
|
||||
"DELETE": {
|
||||
"SUCCESS": "Attribute deleted successfully",
|
||||
"ERROR": "Unable to delete attribute. Please try again later"
|
||||
"SUCCESS": "Attribuutin poisto onnistui",
|
||||
"ERROR": "Attribuuttia ei voida poistaa. Yritä uudelleen myöhemmin"
|
||||
},
|
||||
"ATTRIBUTE_SELECT": {
|
||||
"TITLE": "Lisää määritteitä",
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"EMOJI": {
|
||||
"PLACEHOLDER": "Search emojis",
|
||||
"NOT_FOUND": "No emoji match your search"
|
||||
"NOT_FOUND": "No emoji match your search",
|
||||
"REMOVE": "Poista"
|
||||
}
|
||||
}
|
||||
|
||||
@ -365,7 +365,7 @@
|
||||
"NAME": {
|
||||
"LABEL": "Nimi",
|
||||
"PLACEHOLDER": "Category name",
|
||||
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
|
||||
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
|
||||
"ERROR": "Name is required"
|
||||
},
|
||||
"SLUG": {
|
||||
@ -396,7 +396,7 @@
|
||||
"NAME": {
|
||||
"LABEL": "Nimi",
|
||||
"PLACEHOLDER": "Category name",
|
||||
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
|
||||
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
|
||||
"ERROR": "Name is required"
|
||||
},
|
||||
"SLUG": {
|
||||
|
||||
@ -410,7 +410,7 @@
|
||||
"FOR_EG": "For eg:",
|
||||
"FRIENDLY": {
|
||||
"TITLE": "Friendly",
|
||||
"FROM": "from",
|
||||
"FROM": "lähettäjä",
|
||||
"SUBTITLE": "Add the name of the agent who sent the reply in the sender name to make it friendly."
|
||||
},
|
||||
"PROFESSIONAL": {
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
"BOT_LABEL": "Botti",
|
||||
"READ_MORE": "Read more",
|
||||
"WROTE": "wrote:",
|
||||
"FROM": "from",
|
||||
"FROM": "lähettäjä",
|
||||
"EMAIL": "sähköposti"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"EMOJI": {
|
||||
"PLACEHOLDER": "Rechercher des émojis",
|
||||
"NOT_FOUND": "Aucun émoji ne correspond à votre recherche"
|
||||
"NOT_FOUND": "Aucun émoji ne correspond à votre recherche",
|
||||
"REMOVE": "Supprimer"
|
||||
}
|
||||
}
|
||||
|
||||
@ -365,7 +365,7 @@
|
||||
"NAME": {
|
||||
"LABEL": "Nom",
|
||||
"PLACEHOLDER": "Nom de la catégorie",
|
||||
"HELP_TEXT": "Le nom de la catégorie sera utilisé dans le portail public pour classer les articles.",
|
||||
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
|
||||
"ERROR": "Le nom est requis"
|
||||
},
|
||||
"SLUG": {
|
||||
@ -396,7 +396,7 @@
|
||||
"NAME": {
|
||||
"LABEL": "Nom",
|
||||
"PLACEHOLDER": "Nom de la catégorie",
|
||||
"HELP_TEXT": "Le nom de la catégorie sera utilisé dans le portail public pour classer les articles.",
|
||||
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
|
||||
"ERROR": "Le nom est requis"
|
||||
},
|
||||
"SLUG": {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"EMOJI": {
|
||||
"PLACEHOLDER": "חפש אימוג'י",
|
||||
"NOT_FOUND": "אין אמוג'י שתואם את החיפוש שלך"
|
||||
"NOT_FOUND": "אין אמוג'י שתואם את החיפוש שלך",
|
||||
"REMOVE": "הסר"
|
||||
}
|
||||
}
|
||||
|
||||
@ -365,7 +365,7 @@
|
||||
"NAME": {
|
||||
"LABEL": "שם",
|
||||
"PLACEHOLDER": "שם קטגוריה",
|
||||
"HELP_TEXT": "שם הקטגוריה ישמש בפורטל הפונה לציבור כדי לסווג מאמרים.",
|
||||
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
|
||||
"ERROR": "שם שדה חובה"
|
||||
},
|
||||
"SLUG": {
|
||||
@ -396,7 +396,7 @@
|
||||
"NAME": {
|
||||
"LABEL": "שם",
|
||||
"PLACEHOLDER": "שם קטגוריה",
|
||||
"HELP_TEXT": "שם הקטגוריה ישמש בפורטל הפונה לציבור כדי לסווג מאמרים.",
|
||||
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
|
||||
"ERROR": "שם שדה חובה"
|
||||
},
|
||||
"SLUG": {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"EMOJI": {
|
||||
"PLACEHOLDER": "Search emojis",
|
||||
"NOT_FOUND": "No emoji match your search"
|
||||
"NOT_FOUND": "No emoji match your search",
|
||||
"REMOVE": "Remove"
|
||||
}
|
||||
}
|
||||
|
||||
@ -365,7 +365,7 @@
|
||||
"NAME": {
|
||||
"LABEL": "Name",
|
||||
"PLACEHOLDER": "Category name",
|
||||
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
|
||||
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
|
||||
"ERROR": "Name is required"
|
||||
},
|
||||
"SLUG": {
|
||||
@ -396,7 +396,7 @@
|
||||
"NAME": {
|
||||
"LABEL": "Name",
|
||||
"PLACEHOLDER": "Category name",
|
||||
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
|
||||
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
|
||||
"ERROR": "Name is required"
|
||||
},
|
||||
"SLUG": {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"EMOJI": {
|
||||
"PLACEHOLDER": "Search emojis",
|
||||
"NOT_FOUND": "No emoji match your search"
|
||||
"NOT_FOUND": "No emoji match your search",
|
||||
"REMOVE": "Izbaci"
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user