Merge branch 'release/3.3.0'

This commit is contained in:
Sojan 2023-11-17 20:37:25 -08:00
commit bda2d1d9a4
409 changed files with 6262 additions and 2530 deletions

View File

@ -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

View File

@ -4,6 +4,7 @@ module.exports = {
'prettier',
'plugin:vue/recommended',
'plugin:storybook/recommended',
'plugin:cypress/recommended',
],
parserOptions: {
parser: '@babel/eslint-parser',

12
Gemfile
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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?

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 }) {

View File

@ -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;

View File

@ -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,

View File

@ -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,

View File

@ -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', () => {

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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;
},

View File

@ -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,

View File

@ -78,6 +78,7 @@
variant="clear"
color-scheme="secondary"
icon="add"
:data-testid="menuItem.dataTestid"
@click="e => newLinkClick(e, navigate)"
>
{{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }}

View File

@ -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.

View File

@ -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()) {

View File

@ -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>

View File

@ -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,
},
});
},
},
};

View File

@ -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>

View File

@ -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;
}

View File

@ -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;

View File

@ -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;

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 };

View File

@ -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;

View File

@ -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>

View File

@ -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',
};

View File

@ -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',

View File

@ -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;
}
};

View File

@ -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];

View File

@ -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);
});
});
});

View File

@ -1,6 +1,7 @@
{
"EMOJI": {
"PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search"
"NOT_FOUND": "No emoji match your search",
"REMOVE": "Remove"
}
}

View File

@ -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": {

View File

@ -1,6 +1,7 @@
{
"EMOJI": {
"PLACEHOLDER": "ابحث في الايموجي",
"NOT_FOUND": "لا يوجد إيموجي يطابق بحثك"
"NOT_FOUND": "لا يوجد إيموجي يطابق بحثك",
"REMOVE": "حذف"
}
}

View File

@ -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": {

View File

@ -1,6 +1,7 @@
{
"EMOJI": {
"PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search"
"NOT_FOUND": "No emoji match your search",
"REMOVE": "Remove"
}
}

View File

@ -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": {

View File

@ -1,6 +1,7 @@
{
"EMOJI": {
"PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search"
"NOT_FOUND": "No emoji match your search",
"REMOVE": "Suprimeix"
}
}

View File

@ -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": {

View File

@ -1,6 +1,7 @@
{
"EMOJI": {
"PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search"
"NOT_FOUND": "No emoji match your search",
"REMOVE": "Odebrat"
}
}

View File

@ -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": {

View File

@ -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"
}
}

View File

@ -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": {

View File

@ -1,6 +1,7 @@
{
"EMOJI": {
"PLACEHOLDER": "Emojis durchsuchen",
"NOT_FOUND": "Kein Emoji entspricht deiner Suche"
"NOT_FOUND": "Kein Emoji entspricht deiner Suche",
"REMOVE": "Entfernen"
}
}

View File

@ -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": {

View File

@ -1,6 +1,7 @@
{
"EMOJI": {
"PLACEHOLDER": "Αναζήτηση emojis",
"NOT_FOUND": "Κανένα emoji δεν ταιριάζει με την αναζήτησή σας"
"NOT_FOUND": "Κανένα emoji δεν ταιριάζει με την αναζήτησή σας",
"REMOVE": "Διαγραφή"
}
}

View File

@ -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": {

View File

@ -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."
}
}
}

View File

@ -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"
}
}
}

View File

@ -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."
}
}
}

View File

@ -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 "
}
}
}

View File

@ -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",

View File

@ -1,6 +1,7 @@
{
"EMOJI": {
"PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search"
"NOT_FOUND": "No emoji match your search",
"REMOVE": "Remove"
}
}

View File

@ -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.",

View File

@ -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",

View File

@ -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"
}
}

View File

@ -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."
}
}
}

View File

@ -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": {

View File

@ -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.",

View File

@ -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": "شرکت کنندگان را انتخاب کنید",

View File

@ -1,6 +1,7 @@
{
"EMOJI": {
"PLACEHOLDER": "جستجوی ایموجی",
"NOT_FOUND": "هیچ ایموجی با جستجوی شما مطابقت ندارد"
"NOT_FOUND": "هیچ ایموجی با جستجوی شما مطابقت ندارد",
"REMOVE": "حذف"
}
}

View File

@ -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": "روشن",

View File

@ -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": {

View File

@ -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": {

View File

@ -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.",

View File

@ -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"

View File

@ -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",

View File

@ -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 "
}
}
}

View File

@ -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>."
},

View File

@ -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ä",

View File

@ -1,6 +1,7 @@
{
"EMOJI": {
"PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search"
"NOT_FOUND": "No emoji match your search",
"REMOVE": "Poista"
}
}

View File

@ -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": {

View File

@ -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": {

View File

@ -19,7 +19,7 @@
"BOT_LABEL": "Botti",
"READ_MORE": "Read more",
"WROTE": "wrote:",
"FROM": "from",
"FROM": "lähettäjä",
"EMAIL": "sähköposti"
}
}

View File

@ -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"
}
}

View File

@ -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": {

View File

@ -1,6 +1,7 @@
{
"EMOJI": {
"PLACEHOLDER": "חפש אימוג'י",
"NOT_FOUND": "אין אמוג'י שתואם את החיפוש שלך"
"NOT_FOUND": "אין אמוג'י שתואם את החיפוש שלך",
"REMOVE": "הסר"
}
}

View File

@ -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": {

View File

@ -1,6 +1,7 @@
{
"EMOJI": {
"PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search"
"NOT_FOUND": "No emoji match your search",
"REMOVE": "Remove"
}
}

View File

@ -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": {

View File

@ -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