Merge branch 'release/2.2.0'
This commit is contained in:
@@ -22,6 +22,9 @@ checks:
|
||||
enabled: true
|
||||
config:
|
||||
threshold: 300
|
||||
method-lines:
|
||||
config:
|
||||
threshold: 50
|
||||
exclude_patterns:
|
||||
- 'spec/'
|
||||
- '**/specs/'
|
||||
@@ -44,3 +47,6 @@ exclude_patterns:
|
||||
- 'app/javascript/shared/constants/countries.js'
|
||||
- 'app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/languages.js'
|
||||
- 'app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js'
|
||||
- 'app/javascript/dashboard/routes/dashboard/settings/automation/constants.js'
|
||||
- 'app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.js'
|
||||
- 'app/javascript/dashboard/routes/dashboard/settings/reports/constants.js'
|
||||
|
||||
2
Gemfile
2
Gemfile
@@ -102,7 +102,7 @@ gem 'sentry-ruby'
|
||||
gem 'sentry-sidekiq'
|
||||
|
||||
##-- background job processing --##
|
||||
gem 'sidekiq'
|
||||
gem 'sidekiq', '~> 6.4.0'
|
||||
# We want cron jobs
|
||||
gem 'sidekiq-cron'
|
||||
|
||||
|
||||
122
Gemfile.lock
122
Gemfile.lock
@@ -9,63 +9,63 @@ GIT
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (6.1.4.3)
|
||||
actionpack (= 6.1.4.3)
|
||||
activesupport (= 6.1.4.3)
|
||||
actioncable (6.1.4.6)
|
||||
actionpack (= 6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (6.1.4.3)
|
||||
actionpack (= 6.1.4.3)
|
||||
activejob (= 6.1.4.3)
|
||||
activerecord (= 6.1.4.3)
|
||||
activestorage (= 6.1.4.3)
|
||||
activesupport (= 6.1.4.3)
|
||||
actionmailbox (6.1.4.6)
|
||||
actionpack (= 6.1.4.6)
|
||||
activejob (= 6.1.4.6)
|
||||
activerecord (= 6.1.4.6)
|
||||
activestorage (= 6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
mail (>= 2.7.1)
|
||||
actionmailer (6.1.4.3)
|
||||
actionpack (= 6.1.4.3)
|
||||
actionview (= 6.1.4.3)
|
||||
activejob (= 6.1.4.3)
|
||||
activesupport (= 6.1.4.3)
|
||||
actionmailer (6.1.4.6)
|
||||
actionpack (= 6.1.4.6)
|
||||
actionview (= 6.1.4.6)
|
||||
activejob (= 6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (6.1.4.3)
|
||||
actionview (= 6.1.4.3)
|
||||
activesupport (= 6.1.4.3)
|
||||
actionpack (6.1.4.6)
|
||||
actionview (= 6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
rack (~> 2.0, >= 2.0.9)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (6.1.4.3)
|
||||
actionpack (= 6.1.4.3)
|
||||
activerecord (= 6.1.4.3)
|
||||
activestorage (= 6.1.4.3)
|
||||
activesupport (= 6.1.4.3)
|
||||
actiontext (6.1.4.6)
|
||||
actionpack (= 6.1.4.6)
|
||||
activerecord (= 6.1.4.6)
|
||||
activestorage (= 6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (6.1.4.3)
|
||||
activesupport (= 6.1.4.3)
|
||||
actionview (6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
active_record_query_trace (1.8)
|
||||
activejob (6.1.4.3)
|
||||
activesupport (= 6.1.4.3)
|
||||
activejob (6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (6.1.4.3)
|
||||
activesupport (= 6.1.4.3)
|
||||
activerecord (6.1.4.3)
|
||||
activemodel (= 6.1.4.3)
|
||||
activesupport (= 6.1.4.3)
|
||||
activemodel (6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
activerecord (6.1.4.6)
|
||||
activemodel (= 6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
activerecord-import (1.2.0)
|
||||
activerecord (>= 3.2)
|
||||
activestorage (6.1.4.3)
|
||||
actionpack (= 6.1.4.3)
|
||||
activejob (= 6.1.4.3)
|
||||
activerecord (= 6.1.4.3)
|
||||
activesupport (= 6.1.4.3)
|
||||
activestorage (6.1.4.6)
|
||||
actionpack (= 6.1.4.6)
|
||||
activejob (= 6.1.4.6)
|
||||
activerecord (= 6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
marcel (~> 1.0.0)
|
||||
mini_mime (>= 1.1.0)
|
||||
activesupport (6.1.4.3)
|
||||
activesupport (6.1.4.6)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
@@ -300,7 +300,7 @@ GEM
|
||||
mime-types (~> 3.0)
|
||||
multi_xml (>= 0.5.2)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.8.11)
|
||||
i18n (1.9.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
image_processing (1.12.1)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
@@ -346,7 +346,7 @@ GEM
|
||||
listen (3.7.0)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
loofah (2.13.0)
|
||||
loofah (2.14.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.7.1)
|
||||
@@ -400,7 +400,7 @@ GEM
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (4.0.6)
|
||||
puma (5.5.2)
|
||||
puma (5.6.2)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.1.1)
|
||||
activesupport (>= 3.0.0)
|
||||
@@ -416,29 +416,29 @@ GEM
|
||||
rack-test (1.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-timeout (0.6.0)
|
||||
rails (6.1.4.3)
|
||||
actioncable (= 6.1.4.3)
|
||||
actionmailbox (= 6.1.4.3)
|
||||
actionmailer (= 6.1.4.3)
|
||||
actionpack (= 6.1.4.3)
|
||||
actiontext (= 6.1.4.3)
|
||||
actionview (= 6.1.4.3)
|
||||
activejob (= 6.1.4.3)
|
||||
activemodel (= 6.1.4.3)
|
||||
activerecord (= 6.1.4.3)
|
||||
activestorage (= 6.1.4.3)
|
||||
activesupport (= 6.1.4.3)
|
||||
rails (6.1.4.6)
|
||||
actioncable (= 6.1.4.6)
|
||||
actionmailbox (= 6.1.4.6)
|
||||
actionmailer (= 6.1.4.6)
|
||||
actionpack (= 6.1.4.6)
|
||||
actiontext (= 6.1.4.6)
|
||||
actionview (= 6.1.4.6)
|
||||
activejob (= 6.1.4.6)
|
||||
activemodel (= 6.1.4.6)
|
||||
activerecord (= 6.1.4.6)
|
||||
activestorage (= 6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 6.1.4.3)
|
||||
railties (= 6.1.4.6)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.4.2)
|
||||
loofah (~> 2.3)
|
||||
railties (6.1.4.3)
|
||||
actionpack (= 6.1.4.3)
|
||||
activesupport (= 6.1.4.3)
|
||||
railties (6.1.4.6)
|
||||
actionpack (= 6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
method_source
|
||||
rake (>= 0.13)
|
||||
thor (~> 1.0)
|
||||
@@ -447,7 +447,7 @@ GEM
|
||||
rb-fsevent (0.11.0)
|
||||
rb-inotify (0.10.1)
|
||||
ffi (~> 1.0)
|
||||
redis (4.4.0)
|
||||
redis (4.5.1)
|
||||
redis-namespace (1.8.1)
|
||||
redis (>= 3.0.4)
|
||||
regexp_parser (2.1.1)
|
||||
@@ -546,7 +546,7 @@ GEM
|
||||
sexp_processor (4.15.3)
|
||||
shoulda-matchers (5.0.0)
|
||||
activesupport (>= 5.2.0)
|
||||
sidekiq (6.2.2)
|
||||
sidekiq (6.4.0)
|
||||
connection_pool (>= 2.2.2)
|
||||
rack (~> 2.0)
|
||||
redis (>= 4.2.0)
|
||||
@@ -583,7 +583,7 @@ GEM
|
||||
squasher (0.6.2)
|
||||
statsd-ruby (1.5.0)
|
||||
telephone_number (1.4.12)
|
||||
thor (1.1.0)
|
||||
thor (1.2.1)
|
||||
tilt (2.0.10)
|
||||
time_diff (0.3.0)
|
||||
activesupport
|
||||
@@ -635,7 +635,7 @@ GEM
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
wisper (2.0.0)
|
||||
zeitwerk (2.5.1)
|
||||
zeitwerk (2.5.4)
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin-20
|
||||
@@ -726,7 +726,7 @@ DEPENDENCIES
|
||||
sentry-ruby
|
||||
sentry-sidekiq
|
||||
shoulda-matchers
|
||||
sidekiq
|
||||
sidekiq (~> 6.4.0)
|
||||
sidekiq-cron
|
||||
simplecov (= 0.17.1)
|
||||
slack-ruby-client
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
class AccountBuilder
|
||||
include CustomExceptions::Account
|
||||
pattr_initialize [:account_name!, :email!, :confirmed, :user, :user_full_name, :user_password]
|
||||
pattr_initialize [:account_name!, :email!, :confirmed, :user, :user_full_name, :user_password, :super_admin]
|
||||
|
||||
def perform
|
||||
if @user.nil?
|
||||
@@ -65,6 +65,7 @@ class AccountBuilder
|
||||
password: user_password,
|
||||
password_confirmation: user_password,
|
||||
name: @user_full_name)
|
||||
@user.type = 'SuperAdmin' if @super_admin
|
||||
@user.confirm if @confirmed
|
||||
@user.save!
|
||||
end
|
||||
|
||||
@@ -4,7 +4,7 @@ class ContactInboxBuilder
|
||||
def perform
|
||||
@contact = Contact.find(contact_id)
|
||||
@inbox = @contact.account.inboxes.find(inbox_id)
|
||||
return unless ['Channel::TwilioSms', 'Channel::Email', 'Channel::Api', 'Channel::Whatsapp'].include? @inbox.channel_type
|
||||
return unless ['Channel::TwilioSms', 'Channel::Sms', 'Channel::Email', 'Channel::Api', 'Channel::Whatsapp'].include? @inbox.channel_type
|
||||
|
||||
source_id = @source_id || generate_source_id
|
||||
create_contact_inbox(source_id) if source_id.present?
|
||||
@@ -13,12 +13,18 @@ class ContactInboxBuilder
|
||||
private
|
||||
|
||||
def generate_source_id
|
||||
return twilio_source_id if @inbox.channel_type == 'Channel::TwilioSms'
|
||||
return wa_source_id if @inbox.channel_type == 'Channel::Whatsapp'
|
||||
return @contact.email if @inbox.channel_type == 'Channel::Email'
|
||||
return SecureRandom.uuid if @inbox.channel_type == 'Channel::Api'
|
||||
|
||||
nil
|
||||
case @inbox.channel_type
|
||||
when 'Channel::TwilioSms'
|
||||
twilio_source_id
|
||||
when 'Channel::Whatsapp'
|
||||
wa_source_id
|
||||
when 'Channel::Email'
|
||||
@contact.email
|
||||
when 'Channel::Sms'
|
||||
@contact.phone_number
|
||||
when 'Channel::Api'
|
||||
SecureRandom.uuid
|
||||
end
|
||||
end
|
||||
|
||||
def wa_source_id
|
||||
|
||||
@@ -29,11 +29,12 @@ class Messages::MessageBuilder
|
||||
return if @attachments.blank?
|
||||
|
||||
@attachments.each do |uploaded_attachment|
|
||||
@message.attachments.build(
|
||||
attachment = @message.attachments.build(
|
||||
account_id: @message.account_id,
|
||||
file_type: file_type(uploaded_attachment&.content_type),
|
||||
file: uploaded_attachment
|
||||
)
|
||||
|
||||
attachment.file_type = file_type(uploaded_attachment&.content_type) if uploaded_attachment.is_a?(ActionDispatch::Http::UploadedFile)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ class V2::ReportBuilder
|
||||
include DateRangeHelper
|
||||
attr_reader :account, :params
|
||||
|
||||
DEFAULT_GROUP_BY = 'day'.freeze
|
||||
|
||||
def initialize(account, params)
|
||||
@account = account
|
||||
@params = params
|
||||
@@ -64,26 +66,30 @@ class V2::ReportBuilder
|
||||
|
||||
def conversations_count
|
||||
scope.conversations
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.group_by_period(params[:group_by] || DEFAULT_GROUP_BY,
|
||||
:created_at, range: range, default_value: 0, permit: %w[day week month year])
|
||||
.count
|
||||
end
|
||||
|
||||
def incoming_messages_count
|
||||
scope.messages.incoming.unscope(:order)
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.group_by_period(params[:group_by] || DEFAULT_GROUP_BY,
|
||||
:created_at, range: range, default_value: 0, permit: %w[day week month year])
|
||||
.count
|
||||
end
|
||||
|
||||
def outgoing_messages_count
|
||||
scope.messages.outgoing.unscope(:order)
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.group_by_period(params[:group_by] || DEFAULT_GROUP_BY,
|
||||
:created_at, range: range, default_value: 0, permit: %w[day week month year])
|
||||
.count
|
||||
end
|
||||
|
||||
def resolutions_count
|
||||
scope.conversations
|
||||
.resolved
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.group_by_period(params[:group_by] || DEFAULT_GROUP_BY,
|
||||
:created_at, range: range, default_value: 0, permit: %w[day week month year])
|
||||
.count
|
||||
end
|
||||
|
||||
|
||||
@@ -1,21 +1,44 @@
|
||||
class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseController
|
||||
before_action :check_authorization
|
||||
before_action :fetch_automation_rule, only: [:show, :update, :destroy, :clone]
|
||||
|
||||
def index
|
||||
@automation_rules = Current.account.automation_rules
|
||||
@automation_rules = Current.account.automation_rules.active
|
||||
end
|
||||
|
||||
def create
|
||||
@automation_rule = Current.account.automation_rules.create(automation_rules_permit)
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def update
|
||||
@automation_rule.update(automation_rules_permit)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@automation_rule.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
def clone
|
||||
automation_rule = Current.account.automation_rules.find_by(id: params[:automation_rule_id])
|
||||
new_rule = automation_rule.dup
|
||||
new_rule.save
|
||||
@automation_rule = new_rule
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def automation_rules_permit
|
||||
params.permit(
|
||||
:name, :description, :event_name, :account_id,
|
||||
conditions: [:attribute_key, :filter_operator, :query_operator, { values: [] }],
|
||||
actions: [:action_name, { action_params: [:intiated_at] }]
|
||||
actions: [:action_name, { action_params: [] }]
|
||||
)
|
||||
end
|
||||
|
||||
def fetch_automation_rule
|
||||
@automation_rule = Current.account.automation_rules.find_by(id: params[:id])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,7 +18,7 @@ class Api::V1::Accounts::CampaignsController < Api::V1::Accounts::BaseController
|
||||
def show; end
|
||||
|
||||
def update
|
||||
@campaign.update(campaign_params)
|
||||
@campaign.update!(campaign_params)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -61,6 +61,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
else
|
||||
@status = @conversation.toggle_status
|
||||
end
|
||||
assign_conversation if @conversation.status == 'open' && Current.user.is_a?(User) && Current.user&.agent?
|
||||
end
|
||||
|
||||
def toggle_typing_status
|
||||
@@ -93,6 +94,11 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
@conversation.snoozed_until = parse_date_time(params[:snoozed_until].to_s) if params[:snoozed_until]
|
||||
end
|
||||
|
||||
def assign_conversation
|
||||
@agent = Current.account.users.find(current_user.id)
|
||||
@conversation.update_assignee(@agent)
|
||||
end
|
||||
|
||||
def trigger_typing_event(event, is_private)
|
||||
user = current_user.presence || @resource
|
||||
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: @conversation, user: user, is_private: is_private)
|
||||
|
||||
@@ -2,6 +2,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
include Api::V1::InboxesHelper
|
||||
before_action :fetch_inbox, except: [:index, :create]
|
||||
before_action :fetch_agent_bot, only: [:set_agent_bot]
|
||||
before_action :validate_limit, only: [:create]
|
||||
# we are already handling the authorization in fetch inbox
|
||||
before_action :check_authorization, except: [:show]
|
||||
|
||||
@@ -91,20 +92,9 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def create_channel
|
||||
case permitted_params[:channel][:type]
|
||||
when 'web_widget'
|
||||
Current.account.web_widgets.create!(permitted_params(Channel::WebWidget::EDITABLE_ATTRS)[:channel].except(:type))
|
||||
when 'api'
|
||||
Current.account.api_channels.create!(permitted_params(Channel::Api::EDITABLE_ATTRS)[:channel].except(:type))
|
||||
when 'email'
|
||||
Current.account.email_channels.create!(permitted_params(Channel::Email::EDITABLE_ATTRS)[:channel].except(:type))
|
||||
when 'line'
|
||||
Current.account.line_channels.create!(permitted_params(Channel::Line::EDITABLE_ATTRS)[:channel].except(:type))
|
||||
when 'telegram'
|
||||
Current.account.telegram_channels.create!(permitted_params(Channel::Telegram::EDITABLE_ATTRS)[:channel].except(:type))
|
||||
when 'whatsapp'
|
||||
Current.account.whatsapp_channels.create!(permitted_params(Channel::Whatsapp::EDITABLE_ATTRS)[:channel].except(:type))
|
||||
end
|
||||
return unless %w[web_widget api email line telegram whatsapp sms].include?(permitted_params[:channel][:type])
|
||||
|
||||
account_channels_method.create!(permitted_params(channel_type_from_params::EDITABLE_ATTRS)[:channel].except(:type))
|
||||
end
|
||||
|
||||
def update_channel_feature_flags
|
||||
@@ -123,6 +113,30 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
)
|
||||
end
|
||||
|
||||
def channel_type_from_params
|
||||
{
|
||||
'web_widget' => Channel::WebWidget,
|
||||
'api' => Channel::Api,
|
||||
'email' => Channel::Email,
|
||||
'line' => Channel::Line,
|
||||
'telegram' => Channel::Telegram,
|
||||
'whatsapp' => Channel::Whatsapp,
|
||||
'sms' => Channel::Sms
|
||||
}[permitted_params[:channel][:type]]
|
||||
end
|
||||
|
||||
def account_channels_method
|
||||
{
|
||||
'web_widget' => Current.account.web_widgets,
|
||||
'api' => Current.account.api_channels,
|
||||
'email' => Current.account.email_channels,
|
||||
'line' => Current.account.line_channels,
|
||||
'telegram' => Current.account.telegram_channels,
|
||||
'whatsapp' => Current.account.whatsapp_channels,
|
||||
'sms' => Current.account.sms_channels
|
||||
}[permitted_params[:channel][:type]]
|
||||
end
|
||||
|
||||
def get_channel_attributes(channel_type)
|
||||
if channel_type.constantize.const_defined?('EDITABLE_ATTRS')
|
||||
channel_type.constantize::EDITABLE_ATTRS.presence
|
||||
@@ -130,4 +144,10 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def validate_limit
|
||||
return unless Current.account.inboxes.count >= Current.account.usage_limits[:inboxes]
|
||||
|
||||
render_payment_required('Account limit exceeded. Upgrade to a higher plan')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -38,6 +38,7 @@ class Api::V1::ProfilesController < Api::BaseController
|
||||
:name,
|
||||
:display_name,
|
||||
:avatar,
|
||||
:message_signature,
|
||||
ui_settings: {}
|
||||
)
|
||||
end
|
||||
|
||||
@@ -29,11 +29,12 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||
return if params[:message][:attachments].blank?
|
||||
|
||||
params[:message][:attachments].each do |uploaded_attachment|
|
||||
@message.attachments.new(
|
||||
attachment = @message.attachments.new(
|
||||
account_id: @message.account_id,
|
||||
file_type: helpers.file_type(uploaded_attachment&.content_type),
|
||||
file: uploaded_attachment
|
||||
)
|
||||
|
||||
attachment.file_type = helpers.file_type(uploaded_attachment&.content_type) if uploaded_attachment.is_a?(ActionDispatch::Http::UploadedFile)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -46,7 +46,8 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||
type: params[:type].to_sym,
|
||||
since: params[:since],
|
||||
until: params[:until],
|
||||
id: params[:id]
|
||||
id: params[:id],
|
||||
group_by: params[:group_by]
|
||||
}
|
||||
end
|
||||
|
||||
@@ -56,7 +57,8 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||
type: params[:type].to_sym,
|
||||
since: params[:since],
|
||||
until: params[:until],
|
||||
id: params[:id]
|
||||
id: params[:id],
|
||||
group_by: params[:group_by]
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -25,7 +25,8 @@ class DashboardController < ActionController::Base
|
||||
'API_CHANNEL_NAME',
|
||||
'API_CHANNEL_THUMBNAIL',
|
||||
'ANALYTICS_TOKEN',
|
||||
'ANALYTICS_HOST'
|
||||
'ANALYTICS_HOST',
|
||||
'DIRECT_UPLOADS_ENABLED'
|
||||
).merge(app_config)
|
||||
end
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ class Installation::OnboardingController < ApplicationController
|
||||
user_full_name: onboarding_params.dig(:user, :name),
|
||||
email: onboarding_params.dig(:user, :email),
|
||||
user_password: params.dig(:user, :password),
|
||||
super_admin: true,
|
||||
confirmed: true
|
||||
).perform
|
||||
rescue StandardError => e
|
||||
|
||||
@@ -13,7 +13,8 @@ class Platform::Api::V1::UsersController < PlatformController
|
||||
end
|
||||
|
||||
def login
|
||||
render json: { url: "#{ENV['FRONTEND_URL']}/app/login?email=#{@resource.email}&sso_auth_token=#{@resource.generate_sso_auth_token}" }
|
||||
encoded_email = ERB::Util.url_encode(@resource.email)
|
||||
render json: { url: "#{ENV['FRONTEND_URL']}/app/login?email=#{encoded_email}&sso_auth_token=#{@resource.generate_sso_auth_token}" }
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
@@ -33,11 +33,11 @@ class SuperAdmin::AccountsController < SuperAdmin::ApplicationController
|
||||
# empty values into nil values. It uses other APIs such as `resource_class`
|
||||
# and `dashboard`:
|
||||
#
|
||||
# def resource_params
|
||||
# params.require(resource_class.model_name.param_key).
|
||||
# permit(dashboard.permitted_attributes).
|
||||
# transform_values { |value| value == "" ? nil : value }
|
||||
# end
|
||||
def resource_params
|
||||
permitted_params = super
|
||||
permitted_params[:limits] = permitted_params[:limits].to_h.compact
|
||||
permitted_params
|
||||
end
|
||||
|
||||
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
|
||||
# for more information
|
||||
|
||||
@@ -8,7 +8,7 @@ class SuperAdmin::Devise::SessionsController < Devise::SessionsController
|
||||
def create
|
||||
redirect_to(super_admin_session_path, flash: { error: @error_message }) && return unless valid_credentials?
|
||||
|
||||
sign_in(@super_admin, scope: :super_admin)
|
||||
sign_in(:super_admin, @super_admin)
|
||||
flash.discard
|
||||
redirect_to super_admin_users_path
|
||||
end
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
class SuperAdmin::SuperAdminsController < SuperAdmin::ApplicationController
|
||||
# Overwrite any of the RESTful controller actions to implement custom behavior
|
||||
# For example, you may want to send an email after a foo is updated.
|
||||
#
|
||||
# def update
|
||||
# super
|
||||
# send_foo_updated_email(requested_resource)
|
||||
# end
|
||||
|
||||
# Override this method to specify custom lookup behavior.
|
||||
# This will be used to set the resource for the `show`, `edit`, and `update`
|
||||
# actions.
|
||||
#
|
||||
# def find_resource(param)
|
||||
# Foo.find_by!(slug: param)
|
||||
# end
|
||||
|
||||
# The result of this lookup will be available as `requested_resource`
|
||||
|
||||
# Override this if you have certain roles that require a subset
|
||||
# this will be used to set the records shown on the `index` action.
|
||||
#
|
||||
# def scoped_resource
|
||||
# if current_user.super_admin?
|
||||
# resource_class
|
||||
# else
|
||||
# resource_class.with_less_stuff
|
||||
# end
|
||||
# end
|
||||
|
||||
# Override `resource_params` if you want to transform the submitted
|
||||
# data before it's persisted. For example, the following would turn all
|
||||
# empty values into nil values. It uses other APIs such as `resource_class`
|
||||
# and `dashboard`:
|
||||
#
|
||||
# def resource_params
|
||||
# params.require(resource_class.model_name.param_key).
|
||||
# permit(dashboard.permitted_attributes).
|
||||
# transform_values { |value| value == "" ? nil : value }
|
||||
# end
|
||||
|
||||
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
|
||||
# for more information
|
||||
end
|
||||
@@ -33,12 +33,15 @@ class SuperAdmin::UsersController < SuperAdmin::ApplicationController
|
||||
# empty values into nil values. It uses other APIs such as `resource_class`
|
||||
# and `dashboard`:
|
||||
#
|
||||
# def resource_params
|
||||
# params.require(resource_class.model_name.param_key).
|
||||
# permit(dashboard.permitted_attributes).
|
||||
# transform_values { |value| value == "" ? nil : value }
|
||||
# end
|
||||
def resource_params
|
||||
permitted_params = super
|
||||
permitted_params.delete(:password) if permitted_params[:password].blank?
|
||||
permitted_params
|
||||
end
|
||||
|
||||
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
|
||||
# for more information
|
||||
def find_resource(param)
|
||||
super.becomes(User)
|
||||
end
|
||||
end
|
||||
|
||||
6
app/controllers/webhooks/sms_controller.rb
Normal file
6
app/controllers/webhooks/sms_controller.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
class Webhooks::SmsController < ActionController::API
|
||||
def process_payload
|
||||
Webhooks::SmsEventsJob.perform_later(params['_json']&.first&.to_unsafe_hash)
|
||||
head :ok
|
||||
end
|
||||
end
|
||||
@@ -10,7 +10,7 @@ class WidgetsController < ActionController::Base
|
||||
private
|
||||
|
||||
def set_global_config
|
||||
@global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL')
|
||||
@global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL', 'DIRECT_UPLOADS_ENABLED')
|
||||
end
|
||||
|
||||
def set_web_widget
|
||||
|
||||
@@ -7,6 +7,8 @@ class AccountDashboard < Administrate::BaseDashboard
|
||||
# Each different type represents an Administrate::Field object,
|
||||
# which determines how the attribute is displayed
|
||||
# on pages throughout the dashboard.
|
||||
|
||||
enterprise_attribute_types = ChatwootApp.enterprise? ? { limits: Enterprise::AccountLimitsField } : {}
|
||||
ATTRIBUTE_TYPES = {
|
||||
id: Field::Number,
|
||||
name: Field::String,
|
||||
@@ -16,7 +18,7 @@ class AccountDashboard < Administrate::BaseDashboard
|
||||
conversations: CountField,
|
||||
locale: Field::Select.with_options(collection: LANGUAGES_CONFIG.map { |_x, y| y[:iso_639_1_code] }),
|
||||
account_users: Field::HasMany
|
||||
}.freeze
|
||||
}.merge(enterprise_attribute_types).freeze
|
||||
|
||||
# COLLECTION_ATTRIBUTES
|
||||
# an array of attributes that will be displayed on the model's index page.
|
||||
@@ -33,7 +35,8 @@ class AccountDashboard < Administrate::BaseDashboard
|
||||
|
||||
# SHOW_PAGE_ATTRIBUTES
|
||||
# an array of attributes that will be displayed on the model's show page.
|
||||
SHOW_PAGE_ATTRIBUTES = %i[
|
||||
enterprise_show_page_attributes = ChatwootApp.enterprise? ? %i[limits] : []
|
||||
SHOW_PAGE_ATTRIBUTES = (%i[
|
||||
id
|
||||
name
|
||||
created_at
|
||||
@@ -41,15 +44,16 @@ class AccountDashboard < Administrate::BaseDashboard
|
||||
locale
|
||||
conversations
|
||||
account_users
|
||||
].freeze
|
||||
] + enterprise_show_page_attributes).freeze
|
||||
|
||||
# FORM_ATTRIBUTES
|
||||
# an array of attributes that will be displayed
|
||||
# on the model's form (`new` and `edit`) pages.
|
||||
FORM_ATTRIBUTES = %i[
|
||||
enterprise_form_attributes = ChatwootApp.enterprise? ? %i[limits] : []
|
||||
FORM_ATTRIBUTES = (%i[
|
||||
name
|
||||
locale
|
||||
].freeze
|
||||
] + enterprise_form_attributes).freeze
|
||||
|
||||
# COLLECTION_FILTERS
|
||||
# a hash that defines filters that can be used while searching via the search
|
||||
@@ -69,4 +73,8 @@ class AccountDashboard < Administrate::BaseDashboard
|
||||
def display_resource(account)
|
||||
"##{account.id} #{account.name}"
|
||||
end
|
||||
|
||||
def permitted_attributes
|
||||
super + [limits: {}]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
require 'administrate/base_dashboard'
|
||||
|
||||
class SuperAdminDashboard < Administrate::BaseDashboard
|
||||
# ATTRIBUTE_TYPES
|
||||
# a hash that describes the type of each of the model's fields.
|
||||
#
|
||||
# Each different type represents an Administrate::Field object,
|
||||
# which determines how the attribute is displayed
|
||||
# on pages throughout the dashboard.
|
||||
ATTRIBUTE_TYPES = {
|
||||
id: Field::Number,
|
||||
email: Field::String,
|
||||
password: Field::Password,
|
||||
remember_created_at: Field::DateTime,
|
||||
sign_in_count: Field::Number,
|
||||
current_sign_in_at: Field::DateTime,
|
||||
last_sign_in_at: Field::DateTime,
|
||||
current_sign_in_ip: Field::String.with_options(searchable: false),
|
||||
last_sign_in_ip: Field::String.with_options(searchable: false),
|
||||
created_at: Field::DateTime,
|
||||
updated_at: Field::DateTime
|
||||
}.freeze
|
||||
|
||||
# COLLECTION_ATTRIBUTES
|
||||
# an array of attributes that will be displayed on the model's index page.
|
||||
#
|
||||
# By default, it's limited to four items to reduce clutter on index pages.
|
||||
# Feel free to add, remove, or rearrange items.
|
||||
COLLECTION_ATTRIBUTES = %i[
|
||||
id
|
||||
email
|
||||
].freeze
|
||||
|
||||
# SHOW_PAGE_ATTRIBUTES
|
||||
# an array of attributes that will be displayed on the model's show page.
|
||||
SHOW_PAGE_ATTRIBUTES = %i[
|
||||
id
|
||||
email
|
||||
remember_created_at
|
||||
sign_in_count
|
||||
current_sign_in_at
|
||||
last_sign_in_at
|
||||
current_sign_in_ip
|
||||
last_sign_in_ip
|
||||
created_at
|
||||
updated_at
|
||||
].freeze
|
||||
|
||||
# FORM_ATTRIBUTES
|
||||
# an array of attributes that will be displayed
|
||||
# on the model's form (`new` and `edit`) pages.
|
||||
FORM_ATTRIBUTES = %i[
|
||||
email
|
||||
password
|
||||
].freeze
|
||||
|
||||
# COLLECTION_FILTERS
|
||||
# a hash that defines filters that can be used while searching via the search
|
||||
# field of the dashboard.
|
||||
#
|
||||
# For example to add an option to search for open resources by typing "open:"
|
||||
# in the search field:
|
||||
#
|
||||
# COLLECTION_FILTERS = {
|
||||
# open: ->(resources) { resources.where(open: true) }
|
||||
# }.freeze
|
||||
COLLECTION_FILTERS = {}.freeze
|
||||
|
||||
# Overwrite this method to customize how super admins are displayed
|
||||
# across all pages of the admin dashboard.
|
||||
#
|
||||
# def display_resource(super_admin)
|
||||
# "SuperAdmin ##{super_admin.id}"
|
||||
# end
|
||||
end
|
||||
@@ -30,6 +30,7 @@ class UserDashboard < Administrate::BaseDashboard
|
||||
created_at: Field::DateTime,
|
||||
updated_at: Field::DateTime,
|
||||
pubsub_token: Field::String,
|
||||
type: Field::Select.with_options(collection: [nil, 'SuperAdmin']),
|
||||
accounts: CountField
|
||||
}.freeze
|
||||
|
||||
@@ -44,6 +45,7 @@ class UserDashboard < Administrate::BaseDashboard
|
||||
name
|
||||
email
|
||||
accounts
|
||||
type
|
||||
].freeze
|
||||
|
||||
# SHOW_PAGE_ATTRIBUTES
|
||||
@@ -53,10 +55,12 @@ class UserDashboard < Administrate::BaseDashboard
|
||||
avatar_url
|
||||
unconfirmed_email
|
||||
name
|
||||
type
|
||||
display_name
|
||||
email
|
||||
created_at
|
||||
updated_at
|
||||
confirmed_at
|
||||
account_users
|
||||
].freeze
|
||||
|
||||
@@ -68,6 +72,8 @@ class UserDashboard < Administrate::BaseDashboard
|
||||
display_name
|
||||
email
|
||||
password
|
||||
confirmed_at
|
||||
type
|
||||
].freeze
|
||||
|
||||
# COLLECTION_FILTERS
|
||||
|
||||
7
app/fields/enterprise/account_limits_field.rb
Normal file
7
app/fields/enterprise/account_limits_field.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
require 'administrate/field/base'
|
||||
|
||||
class Enterprise::AccountLimitsField < Administrate::Field::Base
|
||||
def to_s
|
||||
data.present? ? data.to_json : { agents: nil, inboxes: nil }.to_json
|
||||
end
|
||||
end
|
||||
@@ -55,7 +55,7 @@ class ConversationFinder
|
||||
|
||||
def set_inboxes
|
||||
@inbox_ids = if params[:inbox_id]
|
||||
current_account.inboxes.where(id: params[:inbox_id])
|
||||
@current_user.assigned_inboxes.where(id: params[:inbox_id])
|
||||
else
|
||||
@current_user.assigned_inboxes.pluck(:id)
|
||||
end
|
||||
|
||||
@@ -26,8 +26,48 @@ module Api::V1::InboxesHelper
|
||||
def validate_smtp(channel_data)
|
||||
return unless channel_data.key?('smtp_enabled') && channel_data[:smtp_enabled]
|
||||
|
||||
smtp = Net::SMTP.start(channel_data[:smtp_address], channel_data[:smtp_port], channel_data[:smtp_domain], channel_data[:smtp_email],
|
||||
channel_data[:smtp_password], :login)
|
||||
smtp = Net::SMTP.new(channel_data[:smtp_address], channel_data[:smtp_port])
|
||||
|
||||
set_smtp_encryption(channel_data, smtp)
|
||||
|
||||
smtp.start(channel_data[:smtp_domain], channel_data[:smtp_email], channel_data[:smtp_password], :login)
|
||||
smtp.finish unless smtp&.nil?
|
||||
end
|
||||
|
||||
def set_smtp_encryption(channel_data, smtp)
|
||||
if channel_data[:smtp_enable_ssl_tls]
|
||||
set_enable_tls(channel_data, smtp)
|
||||
elsif channel_data[:smtp_enable_starttls_auto]
|
||||
set_enable_starttls_auto(channel_data, smtp)
|
||||
end
|
||||
end
|
||||
|
||||
def set_enable_starttls_auto(channel_data, smtp)
|
||||
return unless smtp.respond_to?(:enable_starttls_auto)
|
||||
|
||||
if channel_data[:smtp_openssl_verify_mode]
|
||||
context = enable_openssl_mode(channel_data[:smtp_openssl_verify_mode])
|
||||
smtp.enable_starttls_auto(context)
|
||||
else
|
||||
smtp.enable_starttls_auto
|
||||
end
|
||||
end
|
||||
|
||||
def set_enable_tls(channel_data, smtp)
|
||||
return unless smtp.respond_to?(:enable_tls)
|
||||
|
||||
if channel_data[:smtp_openssl_verify_mode]
|
||||
context = enable_openssl_mode(channel_data[:smtp_openssl_verify_mode])
|
||||
smtp.enable_tls(context)
|
||||
else
|
||||
smtp.enable_tls
|
||||
end
|
||||
end
|
||||
|
||||
def enable_openssl_mode(smtp_openssl_verify_mode)
|
||||
openssl_verify_mode = "OpenSSL::SSL::VERIFY_#{smtp_openssl_verify_mode.upcase}".constantize if smtp_openssl_verify_mode.is_a?(String)
|
||||
context = Net::SMTP.default_ssl_context
|
||||
context.verify_mode = openssl_verify_mode
|
||||
context
|
||||
end
|
||||
end
|
||||
|
||||
@@ -138,13 +138,14 @@ export default {
|
||||
password,
|
||||
password_confirmation,
|
||||
displayName,
|
||||
avatar,
|
||||
...profileAttributes
|
||||
}) {
|
||||
const formData = new FormData();
|
||||
Object.keys(profileAttributes).forEach(key => {
|
||||
const value = profileAttributes[key];
|
||||
if (value) {
|
||||
formData.append(`profile[${key}]`, value);
|
||||
const hasValue = profileAttributes[key] === undefined;
|
||||
if (!hasValue) {
|
||||
formData.append(`profile[${key}]`, profileAttributes[key]);
|
||||
}
|
||||
});
|
||||
formData.append('profile[display_name]', displayName || '');
|
||||
@@ -152,6 +153,9 @@ export default {
|
||||
formData.append('profile[password]', password);
|
||||
formData.append('profile[password_confirmation]', password_confirmation);
|
||||
}
|
||||
if (avatar) {
|
||||
formData.append('profile[avatar]', avatar);
|
||||
}
|
||||
return axios.put(endPoints('profileUpdate').url, formData);
|
||||
},
|
||||
|
||||
|
||||
14
app/javascript/dashboard/api/automation.js
Normal file
14
app/javascript/dashboard/api/automation.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/* global axios */
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class AutomationsAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('automation_rules', { accountScoped: true });
|
||||
}
|
||||
|
||||
clone(automationId) {
|
||||
return axios.post(`${this.url}/${automationId}/clone`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new AutomationsAPI();
|
||||
18
app/javascript/dashboard/api/customViews.js
Normal file
18
app/javascript/dashboard/api/customViews.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/* global axios */
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class CustomViewsAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('custom_filters', { accountScoped: true });
|
||||
}
|
||||
|
||||
getCustomViewsByFilterType(type) {
|
||||
return axios.get(`${this.url}?filter_type=${type}`);
|
||||
}
|
||||
|
||||
deleteCustomViews(id, type) {
|
||||
return axios.delete(`${this.url}/${id}?filter_type=${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new CustomViewsAPI();
|
||||
@@ -7,17 +7,19 @@ export const buildCreatePayload = ({
|
||||
isPrivate,
|
||||
contentAttributes,
|
||||
echoId,
|
||||
file,
|
||||
files,
|
||||
ccEmails = '',
|
||||
bccEmails = '',
|
||||
}) => {
|
||||
let payload;
|
||||
if (file) {
|
||||
if (files && files.length !== 0) {
|
||||
payload = new FormData();
|
||||
payload.append('attachments[]', file, file.name);
|
||||
if (message) {
|
||||
payload.append('content', message);
|
||||
}
|
||||
files.forEach(file => {
|
||||
payload.append('attachments[]', file);
|
||||
});
|
||||
payload.append('private', isPrivate);
|
||||
payload.append('echo_id', echoId);
|
||||
payload.append('cc_emails', ccEmails);
|
||||
@@ -46,7 +48,7 @@ class MessageApi extends ApiClient {
|
||||
private: isPrivate,
|
||||
contentAttributes,
|
||||
echo_id: echoId,
|
||||
file,
|
||||
files,
|
||||
ccEmails = '',
|
||||
bccEmails = '',
|
||||
}) {
|
||||
@@ -58,7 +60,7 @@ class MessageApi extends ApiClient {
|
||||
isPrivate,
|
||||
contentAttributes,
|
||||
echoId,
|
||||
file,
|
||||
files,
|
||||
ccEmails,
|
||||
bccEmails,
|
||||
}),
|
||||
|
||||
@@ -6,15 +6,15 @@ class ReportsAPI extends ApiClient {
|
||||
super('reports', { accountScoped: true, apiVersion: 'v2' });
|
||||
}
|
||||
|
||||
getReports(metric, since, until, type = 'account', id) {
|
||||
getReports(metric, since, until, type = 'account', id, group_by) {
|
||||
return axios.get(`${this.url}`, {
|
||||
params: { metric, since, until, type, id },
|
||||
params: { metric, since, until, type, id, group_by },
|
||||
});
|
||||
}
|
||||
|
||||
getSummary(since, until, type = 'account', id) {
|
||||
getSummary(since, until, type = 'account', id, group_by) {
|
||||
return axios.get(`${this.url}/summary`, {
|
||||
params: { since, until, type, id },
|
||||
params: { since, until, type, id, group_by },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
15
app/javascript/dashboard/api/specs/automation.spec.js
Normal file
15
app/javascript/dashboard/api/specs/automation.spec.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import automations from '../automation';
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
describe('#AutomationsAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
expect(automations).toBeInstanceOf(ApiClient);
|
||||
expect(automations).toHaveProperty('get');
|
||||
expect(automations).toHaveProperty('show');
|
||||
expect(automations).toHaveProperty('create');
|
||||
expect(automations).toHaveProperty('update');
|
||||
expect(automations).toHaveProperty('delete');
|
||||
expect(automations).toHaveProperty('clone');
|
||||
expect(automations.url).toBe('/api/v1/automation_rules');
|
||||
});
|
||||
});
|
||||
@@ -36,7 +36,7 @@ describe('#ConversationAPI', () => {
|
||||
echoId: 12,
|
||||
isPrivate: true,
|
||||
|
||||
file: new Blob(['test-content'], { type: 'application/pdf' }),
|
||||
files: [new Blob(['test-content'], { type: 'application/pdf' })],
|
||||
});
|
||||
expect(formPayload).toBeInstanceOf(FormData);
|
||||
expect(formPayload.get('content')).toEqual('test content');
|
||||
|
||||
@@ -228,8 +228,14 @@
|
||||
@include flex-align(right, null);
|
||||
|
||||
.wrap {
|
||||
align-items: flex-end;
|
||||
display: flex;
|
||||
margin-right: $space-normal;
|
||||
text-align: right;
|
||||
|
||||
.sender--info {
|
||||
padding: var(--space-small) 0 var(--space-smaller) var(--space-small);
|
||||
}
|
||||
}
|
||||
|
||||
.bubble {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
@include shadow;
|
||||
background-color: $woot-snackbar-bg;
|
||||
border-radius: $space-smaller;
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
margin-bottom: $space-small;
|
||||
max-width: 40rem;
|
||||
min-height: 3rem;
|
||||
|
||||
@@ -1,26 +1,51 @@
|
||||
l<template>
|
||||
<template>
|
||||
<div class="conversations-list-wrap">
|
||||
<slot></slot>
|
||||
<div class="chat-list__top" :class="{ filter__applied: hasAppliedFilters }">
|
||||
<div
|
||||
class="chat-list__top"
|
||||
:class="{ filter__applied: hasAppliedFiltersOrActiveFolders }"
|
||||
>
|
||||
<h1 class="page-title text-truncate" :title="pageTitle">
|
||||
{{ pageTitle }}
|
||||
</h1>
|
||||
|
||||
<div class="filter--actions">
|
||||
<chat-filter
|
||||
v-if="!hasAppliedFilters"
|
||||
v-if="!hasAppliedFiltersOrActiveFolders"
|
||||
@statusFilterChange="updateStatusType"
|
||||
/>
|
||||
<div v-if="hasAppliedFilters && !hasActiveFolders">
|
||||
<woot-button
|
||||
v-tooltip.top-end="$t('FILTER.CUSTOM_VIEWS.ADD.SAVE_BUTTON')"
|
||||
size="tiny"
|
||||
variant="smooth"
|
||||
color-scheme="secondary"
|
||||
icon="save"
|
||||
@click="onClickOpenAddFoldersModal"
|
||||
/>
|
||||
<woot-button
|
||||
v-tooltip.top-end="$t('FILTER.CLEAR_BUTTON_LABEL')"
|
||||
size="tiny"
|
||||
variant="smooth"
|
||||
color-scheme="alert"
|
||||
icon="dismiss-circle"
|
||||
@click="resetAndFetchData"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="hasActiveFolders">
|
||||
<woot-button
|
||||
v-tooltip.top-end="$t('FILTER.CUSTOM_VIEWS.DELETE.DELETE_BUTTON')"
|
||||
size="tiny"
|
||||
variant="smooth"
|
||||
color-scheme="alert"
|
||||
icon="delete"
|
||||
class="delete-custom-view__button"
|
||||
@click="onClickOpenDeleteFoldersModal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<woot-button
|
||||
v-else
|
||||
size="small"
|
||||
variant="clear"
|
||||
color-scheme="alert"
|
||||
@click="resetAndFetchData"
|
||||
>
|
||||
{{ $t('FILTER.CLEAR_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-tooltip.top-end="$t('FILTER.TOOLTIP_LABEL')"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
@@ -33,8 +58,24 @@ l<template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<add-custom-views
|
||||
v-if="showAddFoldersModal"
|
||||
:custom-views-query="foldersQuery"
|
||||
:open-last-saved-item="openLastSavedItemInFolder"
|
||||
@close="onCloseAddFoldersModal"
|
||||
/>
|
||||
|
||||
<delete-custom-views
|
||||
v-if="showDeleteFoldersModal"
|
||||
:show-delete-popup.sync="showDeleteFoldersModal"
|
||||
:active-custom-view="activeFolder"
|
||||
:custom-views-id="foldersId"
|
||||
:open-last-item-after-delete="openLastItemAfterDeleteInFolder"
|
||||
@close="onCloseDeleteFoldersModal"
|
||||
/>
|
||||
|
||||
<chat-type-tabs
|
||||
v-if="!hasAppliedFilters"
|
||||
v-if="!hasAppliedFiltersOrActiveFolders"
|
||||
:items="assigneeTabItems"
|
||||
:active-tab="activeAssigneeTab"
|
||||
class="tab--chat-type"
|
||||
@@ -51,6 +92,7 @@ l<template>
|
||||
:key="chat.id"
|
||||
:active-label="label"
|
||||
:team-id="teamId"
|
||||
:folders-id="foldersId"
|
||||
:chat="chat"
|
||||
:conversation-type="conversationType"
|
||||
:show-assignee="showAssigneeInConversationCard"
|
||||
@@ -87,7 +129,7 @@ l<template>
|
||||
>
|
||||
<conversation-advanced-filter
|
||||
v-if="showAdvancedFilters"
|
||||
:filter-types="advancedFilterTypes"
|
||||
:initial-filter-types="advancedFilterTypes"
|
||||
:on-close="onToggleAdvanceFiltersModal"
|
||||
@applyFilter="onApplyFilter"
|
||||
/>
|
||||
@@ -108,6 +150,8 @@ import conversationMixin from '../mixins/conversations';
|
||||
import wootConstants from '../constants';
|
||||
import advancedFilterTypes from './widgets/conversation/advancedFilterItems';
|
||||
import filterQueryGenerator from '../helper/filterQueryGenerator.js';
|
||||
import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews';
|
||||
import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue';
|
||||
|
||||
import {
|
||||
hasPressedAltAndJKey,
|
||||
@@ -116,10 +160,12 @@ import {
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AddCustomViews,
|
||||
ChatTypeTabs,
|
||||
ConversationCard,
|
||||
ChatFilter,
|
||||
ConversationAdvancedFilter,
|
||||
DeleteCustomViews,
|
||||
},
|
||||
mixins: [timeMixin, conversationMixin, eventListenerMixins],
|
||||
props: {
|
||||
@@ -139,6 +185,10 @@ export default {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
foldersId: {
|
||||
type: [String, Number],
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -149,6 +199,9 @@ export default {
|
||||
...filter,
|
||||
attributeName: this.$t(`FILTER.ATTRIBUTES.${filter.attributeI18nKey}`),
|
||||
})),
|
||||
foldersQuery: {},
|
||||
showAddFoldersModal: false,
|
||||
showDeleteFoldersModal: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -163,10 +216,24 @@ export default {
|
||||
activeInbox: 'getSelectedInbox',
|
||||
conversationStats: 'conversationStats/getStats',
|
||||
appliedFilters: 'getAppliedConversationFilters',
|
||||
folders: 'customViews/getCustomViews',
|
||||
}),
|
||||
hasAppliedFilters() {
|
||||
return this.appliedFilters.length;
|
||||
},
|
||||
hasActiveFolders() {
|
||||
return this.activeFolder && this.foldersId !== 0;
|
||||
},
|
||||
hasAppliedFiltersOrActiveFolders() {
|
||||
return this.hasAppliedFilters || this.hasActiveFolders;
|
||||
},
|
||||
savedFoldersValue() {
|
||||
if (this.hasActiveFolders) {
|
||||
const payload = this.activeFolder.query;
|
||||
this.fetchSavedFilteredConversations(payload);
|
||||
}
|
||||
return {};
|
||||
},
|
||||
assigneeTabItems() {
|
||||
return this.$t('CHAT_LIST.ASSIGNEE_TYPE_TABS').map(item => {
|
||||
const count = this.conversationStats[item.COUNT_KEY] || 0;
|
||||
@@ -178,7 +245,10 @@ export default {
|
||||
});
|
||||
},
|
||||
showAssigneeInConversationCard() {
|
||||
return this.activeAssigneeTab === wootConstants.ASSIGNEE_TYPE.ALL;
|
||||
return (
|
||||
this.hasAppliedFiltersOrActiveFolders ||
|
||||
this.activeAssigneeTab === wootConstants.ASSIGNEE_TYPE.ALL
|
||||
);
|
||||
},
|
||||
inbox() {
|
||||
return this.$store.getters['inboxes/getInbox'](this.activeInbox);
|
||||
@@ -189,7 +259,9 @@ export default {
|
||||
);
|
||||
},
|
||||
currentPageFilterKey() {
|
||||
return this.hasAppliedFilters ? 'appliedFilters' : this.activeAssigneeTab;
|
||||
return this.hasAppliedFiltersOrActiveFolders
|
||||
? 'appliedFilters'
|
||||
: this.activeAssigneeTab;
|
||||
},
|
||||
currentFiltersPage() {
|
||||
return this.$store.getters['conversationPage/getCurrentPageFilter'](
|
||||
@@ -212,6 +284,7 @@ export default {
|
||||
conversationType: this.conversationType
|
||||
? this.conversationType
|
||||
: undefined,
|
||||
folders: this.hasActiveFolders ? this.savedFoldersValue : undefined,
|
||||
};
|
||||
},
|
||||
pageTitle() {
|
||||
@@ -227,11 +300,14 @@ export default {
|
||||
if (this.conversationType === 'mention') {
|
||||
return this.$t('CHAT_LIST.MENTION_HEADING');
|
||||
}
|
||||
if (this.hasActiveFolders) {
|
||||
return this.activeFolder.name;
|
||||
}
|
||||
return this.$t('CHAT_LIST.TAB_HEADING');
|
||||
},
|
||||
conversationList() {
|
||||
let conversationList = [];
|
||||
if (!this.hasAppliedFilters) {
|
||||
if (!this.hasAppliedFiltersOrActiveFolders) {
|
||||
const filters = this.conversationFilters;
|
||||
if (this.activeAssigneeTab === 'me') {
|
||||
conversationList = [...this.mineChatsList(filters)];
|
||||
@@ -246,6 +322,16 @@ export default {
|
||||
|
||||
return conversationList;
|
||||
},
|
||||
activeFolder() {
|
||||
if (this.foldersId) {
|
||||
const activeView = this.folders.filter(
|
||||
view => view.id === Number(this.foldersId)
|
||||
);
|
||||
const [firstValue] = activeView;
|
||||
return firstValue;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
activeTeam() {
|
||||
if (this.teamId) {
|
||||
return this.$store.getters['teams/getTeam'](this.teamId);
|
||||
@@ -266,6 +352,11 @@ export default {
|
||||
conversationType() {
|
||||
this.resetAndFetchData();
|
||||
},
|
||||
activeFolder() {
|
||||
if (!this.hasAppliedFilters) {
|
||||
this.resetAndFetchData();
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('setChatFilter', this.activeStatus);
|
||||
@@ -280,10 +371,23 @@ export default {
|
||||
if (this.$route.name !== 'home') {
|
||||
this.$router.push({ name: 'home' });
|
||||
}
|
||||
this.foldersQuery = filterQueryGenerator(payload);
|
||||
this.$store.dispatch('conversationPage/reset');
|
||||
this.$store.dispatch('emptyAllConversations');
|
||||
this.fetchFilteredConversations(payload);
|
||||
},
|
||||
onClickOpenAddFoldersModal() {
|
||||
this.showAddFoldersModal = true;
|
||||
},
|
||||
onCloseAddFoldersModal() {
|
||||
this.showAddFoldersModal = false;
|
||||
},
|
||||
onClickOpenDeleteFoldersModal() {
|
||||
this.showDeleteFoldersModal = true;
|
||||
},
|
||||
onCloseDeleteFoldersModal() {
|
||||
this.showDeleteFoldersModal = false;
|
||||
},
|
||||
onToggleAdvanceFiltersModal() {
|
||||
this.showAdvancedFilters = !this.showAdvancedFilters;
|
||||
},
|
||||
@@ -335,6 +439,13 @@ export default {
|
||||
this.$store.dispatch('conversationPage/reset');
|
||||
this.$store.dispatch('emptyAllConversations');
|
||||
this.$store.dispatch('clearConversationFilters');
|
||||
if (this.hasActiveFolders) {
|
||||
const payload = this.activeFolder.query;
|
||||
this.fetchSavedFilteredConversations(payload);
|
||||
}
|
||||
if (this.foldersId) {
|
||||
return;
|
||||
}
|
||||
this.fetchConversations();
|
||||
},
|
||||
fetchConversations() {
|
||||
@@ -343,8 +454,12 @@ export default {
|
||||
.then(() => this.$emit('conversation-load'));
|
||||
},
|
||||
loadMoreConversations() {
|
||||
if (!this.hasAppliedFilters) {
|
||||
if (!this.hasAppliedFiltersOrActiveFolders) {
|
||||
this.fetchConversations();
|
||||
}
|
||||
if (this.hasActiveFolders) {
|
||||
const payload = this.activeFolder.query;
|
||||
this.fetchSavedFilteredConversations(payload);
|
||||
} else {
|
||||
this.fetchFilteredConversations(this.appliedFilters);
|
||||
}
|
||||
@@ -359,6 +474,15 @@ export default {
|
||||
.then(() => this.$emit('conversation-load'));
|
||||
this.showAdvancedFilters = false;
|
||||
},
|
||||
fetchSavedFilteredConversations(payload) {
|
||||
let page = this.currentFiltersPage + 1;
|
||||
this.$store
|
||||
.dispatch('fetchFilteredConversations', {
|
||||
queryData: payload,
|
||||
page,
|
||||
})
|
||||
.then(() => this.$emit('conversation-load'));
|
||||
},
|
||||
updateAssigneeTab(selectedTab) {
|
||||
if (this.activeAssigneeTab !== selectedTab) {
|
||||
bus.$emit('clearSearchInput');
|
||||
@@ -374,6 +498,22 @@ export default {
|
||||
this.resetAndFetchData();
|
||||
}
|
||||
},
|
||||
openLastSavedItemInFolder() {
|
||||
const lastItemOfFolder = this.folders[this.folders.length - 1];
|
||||
const lastItemId = lastItemOfFolder.id;
|
||||
this.$router.push({
|
||||
name: 'folder_conversations',
|
||||
params: { id: lastItemId },
|
||||
});
|
||||
},
|
||||
openLastItemAfterDeleteInFolder() {
|
||||
if (this.folders.length > 0) {
|
||||
this.openLastSavedItemInFolder();
|
||||
} else {
|
||||
this.$router.push({ name: 'home' });
|
||||
this.fetchConversations();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -413,7 +553,11 @@ export default {
|
||||
}
|
||||
|
||||
.filter__applied {
|
||||
padding: var(--space-slab) 0 !important;
|
||||
padding: 0 0 var(--space-slab) 0 !important;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.delete-custom-view__button {
|
||||
margin-right: var(--space-normal);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
rel="noopener noreferrer"
|
||||
class="value"
|
||||
>
|
||||
{{ value || '---' }}
|
||||
{{ urlValue }}
|
||||
</a>
|
||||
<p v-else class="value">
|
||||
{{ displayValue || '---' }}
|
||||
@@ -119,7 +119,7 @@ import format from 'date-fns/format';
|
||||
import { required, url } from 'vuelidate/lib/validators';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
|
||||
|
||||
import { isValidURL } from '../helper/URLHelper';
|
||||
const DATE_FORMAT = 'yyyy-MM-dd';
|
||||
|
||||
export default {
|
||||
@@ -141,16 +141,6 @@ export default {
|
||||
editedValue: null,
|
||||
};
|
||||
},
|
||||
validations() {
|
||||
if (this.isAttributeTypeLink) {
|
||||
return {
|
||||
editedValue: { required, url },
|
||||
};
|
||||
}
|
||||
return {
|
||||
editedValue: { required },
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
formattedValue() {
|
||||
@@ -184,6 +174,9 @@ export default {
|
||||
isAttributeTypeDate() {
|
||||
return this.attributeType === 'date';
|
||||
},
|
||||
urlValue() {
|
||||
return isValidURL(this.value) ? this.value : '---';
|
||||
},
|
||||
notAttributeTypeCheckboxAndList() {
|
||||
return !this.isAttributeTypeCheckbox && !this.isAttributeTypeList;
|
||||
},
|
||||
@@ -206,6 +199,23 @@ export default {
|
||||
return this.editedValue;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
value() {
|
||||
this.isEditing = false;
|
||||
this.editedValue = this.value;
|
||||
},
|
||||
},
|
||||
|
||||
validations() {
|
||||
if (this.isAttributeTypeLink) {
|
||||
return {
|
||||
editedValue: { required, url },
|
||||
};
|
||||
}
|
||||
return {
|
||||
editedValue: { required },
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.editedValue = this.formattedValue;
|
||||
bus.$on(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, focusAttributeKey => {
|
||||
|
||||
@@ -4,7 +4,12 @@
|
||||
<div class="ui-notification">
|
||||
<fluent-icon icon="wifi-off" />
|
||||
<p class="ui-notification-text">
|
||||
{{ $t('NETWORK.NOTIFICATION.TEXT') }}
|
||||
{{
|
||||
useInstallationName(
|
||||
$t('NETWORK.NOTIFICATION.TEXT'),
|
||||
globalConfig.installationName
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<woot-button variant="clear" size="small" @click="refreshPage">
|
||||
{{ $t('NETWORK.BUTTON.REFRESH') }}
|
||||
@@ -23,13 +28,22 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
mixins: [globalConfigMixin],
|
||||
|
||||
data() {
|
||||
return {
|
||||
showNotification: !navigator.onLine,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ globalConfig: 'globalConfig/get' }),
|
||||
},
|
||||
|
||||
mounted() {
|
||||
window.addEventListener('offline', this.updateOnlineStatus);
|
||||
},
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
<div class="ui-snackbar-text">
|
||||
{{ message }}
|
||||
</div>
|
||||
<div v-if="action" class="ui-snackbar-action">
|
||||
<router-link v-if="action.type == 'link'" :to="action.to">
|
||||
{{ action.message }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -12,6 +17,10 @@
|
||||
export default {
|
||||
props: {
|
||||
message: { type: String, default: '' },
|
||||
action: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
showButton: Boolean,
|
||||
duration: {
|
||||
type: [String, Number],
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
v-for="snackMessage in snackMessages"
|
||||
:key="snackMessage.key"
|
||||
:message="snackMessage.message"
|
||||
:action="snackMessage.action"
|
||||
/>
|
||||
</transition-group>
|
||||
</template>
|
||||
@@ -35,8 +36,12 @@ export default {
|
||||
bus.$off('newToastMessage', this.onNewToastMessage);
|
||||
},
|
||||
methods: {
|
||||
onNewToastMessage(message) {
|
||||
this.snackMessages.push({ key: new Date().getTime(), message });
|
||||
onNewToastMessage(message, action) {
|
||||
this.snackMessages.push({
|
||||
key: new Date().getTime(),
|
||||
message,
|
||||
action,
|
||||
});
|
||||
window.setTimeout(() => {
|
||||
this.snackMessages.splice(0, 1);
|
||||
}, this.duration);
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
:inboxes="inboxes"
|
||||
:labels="labels"
|
||||
:teams="teams"
|
||||
:custom-views="customViews"
|
||||
:menu-config="activeSecondaryMenu"
|
||||
:current-role="currentRole"
|
||||
@add-label="showAddLabelPopup"
|
||||
@@ -92,6 +93,26 @@ export default {
|
||||
labels: 'labels/getLabelsOnSidebar',
|
||||
teams: 'teams/getMyTeams',
|
||||
}),
|
||||
activeCustomView() {
|
||||
if (this.activePrimaryMenu.key === 'contacts') {
|
||||
return 'contact';
|
||||
}
|
||||
if (this.activePrimaryMenu.key === 'conversations') {
|
||||
return 'conversation';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
customViews() {
|
||||
return this.$store.getters['customViews/getCustomViewsByFilterType'](
|
||||
this.activeCustomView
|
||||
);
|
||||
},
|
||||
isConversationOrContactActive() {
|
||||
return (
|
||||
this.activePrimaryMenu.key === 'contacts' ||
|
||||
this.activePrimaryMenu.key === 'conversations'
|
||||
);
|
||||
},
|
||||
sideMenuConfig() {
|
||||
return getSidebarItems(this.accountId);
|
||||
},
|
||||
@@ -119,15 +140,27 @@ export default {
|
||||
return activePrimaryMenu;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
activeCustomView() {
|
||||
this.fetchCustomViews();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('labels/get');
|
||||
this.$store.dispatch('inboxes/get');
|
||||
this.$store.dispatch('notifications/unReadCount');
|
||||
this.$store.dispatch('teams/get');
|
||||
this.$store.dispatch('attributes/get');
|
||||
this.fetchCustomViews();
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchCustomViews() {
|
||||
if (this.isConversationOrContactActive) {
|
||||
this.$store.dispatch('customViews/get', this.activeCustomView);
|
||||
}
|
||||
},
|
||||
toggleKeyShortcutModal() {
|
||||
this.showShortcutModal = true;
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@ const contacts = accountId => ({
|
||||
routes: [
|
||||
'contacts_dashboard',
|
||||
'contact_profile_dashboard',
|
||||
'contacts_segments_dashboard',
|
||||
'contacts_labels_dashboard',
|
||||
],
|
||||
menuItems: [
|
||||
|
||||
@@ -14,6 +14,8 @@ const conversations = accountId => ({
|
||||
'conversations_through_team',
|
||||
'conversation_mentions',
|
||||
'conversation_through_mentions',
|
||||
'folder_conversations',
|
||||
'conversations_through_folders',
|
||||
],
|
||||
menuItems: [
|
||||
{
|
||||
|
||||
@@ -70,7 +70,7 @@ const settings = accountId => ({
|
||||
toStateName: 'attributes_list',
|
||||
},
|
||||
{
|
||||
icon: 'autocorrect',
|
||||
icon: 'automation',
|
||||
label: 'AUTOMATION',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/automation/list`),
|
||||
|
||||
@@ -40,6 +40,10 @@ export default {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
customViews: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
menuConfig: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
@@ -53,6 +57,9 @@ export default {
|
||||
hasSecondaryMenu() {
|
||||
return this.menuConfig.menuItems && this.menuConfig.menuItems.length;
|
||||
},
|
||||
contactCustomViews() {
|
||||
return this.customViews.filter(view => view.filter_type === 'contact');
|
||||
},
|
||||
accessibleMenuItems() {
|
||||
if (!this.currentRole) {
|
||||
return [];
|
||||
@@ -150,14 +157,57 @@ export default {
|
||||
})),
|
||||
};
|
||||
},
|
||||
foldersSection() {
|
||||
return {
|
||||
icon: 'folder',
|
||||
label: 'CUSTOM_VIEWS_FOLDER',
|
||||
hasSubMenu: true,
|
||||
key: 'custom_view',
|
||||
children: this.customViews
|
||||
.filter(view => view.filter_type === 'conversation')
|
||||
.map(view => ({
|
||||
id: view.id,
|
||||
label: view.name,
|
||||
truncateLabel: true,
|
||||
toState: frontendURL(
|
||||
`accounts/${this.accountId}/custom_view/${view.id}`
|
||||
),
|
||||
})),
|
||||
};
|
||||
},
|
||||
contactSegmentsSection() {
|
||||
return {
|
||||
icon: 'folder',
|
||||
label: 'CUSTOM_VIEWS_SEGMENTS',
|
||||
hasSubMenu: true,
|
||||
key: 'custom_view',
|
||||
children: this.customViews
|
||||
.filter(view => view.filter_type === 'contact')
|
||||
.map(view => ({
|
||||
id: view.id,
|
||||
label: view.name,
|
||||
truncateLabel: true,
|
||||
toState: frontendURL(
|
||||
`accounts/${this.accountId}/contacts/custom_view/${view.id}`
|
||||
),
|
||||
})),
|
||||
};
|
||||
},
|
||||
additionalSecondaryMenuItems() {
|
||||
let conversationMenuItems = [this.inboxSection, this.labelSection];
|
||||
let contactMenuItems = [this.contactLabelSection];
|
||||
if (this.teams.length) {
|
||||
conversationMenuItems = [this.teamSection, ...conversationMenuItems];
|
||||
}
|
||||
if (this.customViews.length) {
|
||||
conversationMenuItems = [this.foldersSection, ...conversationMenuItems];
|
||||
}
|
||||
if (this.contactCustomViews.length) {
|
||||
contactMenuItems = [this.contactSegmentsSection, ...contactMenuItems];
|
||||
}
|
||||
return {
|
||||
conversations: conversationMenuItems,
|
||||
contacts: [this.contactLabelSection],
|
||||
contacts: contactMenuItems,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
v-else
|
||||
class="secondary-menu--title secondary-menu--link fs-small"
|
||||
:class="computedClass"
|
||||
:to="menuItem.toState"
|
||||
:to="menuItem && menuItem.toState"
|
||||
>
|
||||
<fluent-icon
|
||||
:icon="menuItem.icon"
|
||||
@@ -15,6 +15,13 @@
|
||||
size="14"
|
||||
/>
|
||||
{{ $t(`SIDEBAR.${menuItem.label}`) }}
|
||||
<span
|
||||
v-if="menuItem.label === 'AUTOMATION'"
|
||||
data-view-component="true"
|
||||
label="Beta"
|
||||
class="beta"
|
||||
>Beta
|
||||
</span>
|
||||
</router-link>
|
||||
|
||||
<ul v-if="hasSubMenu" class="nested vertical menu">
|
||||
@@ -73,14 +80,48 @@ export default {
|
||||
hasSubMenu() {
|
||||
return !!this.menuItem.children;
|
||||
},
|
||||
isInboxConversation() {
|
||||
return (
|
||||
this.$store.state.route.name === 'inbox_conversation' &&
|
||||
this.menuItem.toStateName === 'home'
|
||||
);
|
||||
},
|
||||
isTeamsSettings() {
|
||||
return (
|
||||
this.$store.state.route.name === 'settings_teams_edit' &&
|
||||
this.menuItem.toStateName === 'settings_teams_list'
|
||||
);
|
||||
},
|
||||
isInboxsSettings() {
|
||||
return (
|
||||
this.$store.state.route.name === 'settings_inbox_show' &&
|
||||
this.menuItem.toStateName === 'settings_inbox_list'
|
||||
);
|
||||
},
|
||||
isIntegrationsSettings() {
|
||||
return (
|
||||
this.$store.state.route.name === 'settings_integrations_webhook' &&
|
||||
this.menuItem.toStateName === 'settings_integrations'
|
||||
);
|
||||
},
|
||||
isApplicationsSettings() {
|
||||
return (
|
||||
this.$store.state.route.name === 'settings_applications_integration' &&
|
||||
this.menuItem.toStateName === 'settings_applications'
|
||||
);
|
||||
},
|
||||
|
||||
computedClass() {
|
||||
// If active Inbox is present
|
||||
// donot highlight conversations
|
||||
if (this.activeInbox) return ' ';
|
||||
|
||||
if (
|
||||
this.$store.state.route.name === 'inbox_conversation' &&
|
||||
this.menuItem.toStateName === 'home'
|
||||
this.isInboxConversation ||
|
||||
this.isTeamsSettings ||
|
||||
this.isInboxsSettings ||
|
||||
this.isIntegrationsSettings ||
|
||||
this.isApplicationsSettings
|
||||
) {
|
||||
return 'is-active';
|
||||
}
|
||||
@@ -187,4 +228,17 @@ export default {
|
||||
color: var(--w-500);
|
||||
}
|
||||
}
|
||||
.beta {
|
||||
padding-right: var(--space-smaller) !important;
|
||||
padding-left: var(--space-smaller) !important;
|
||||
margin-left: var(--space-half) !important;
|
||||
display: inline-block;
|
||||
font-size: var(--font-size-mini);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 18px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 2em;
|
||||
color: var(--g-800);
|
||||
border-color: var(--g-700);
|
||||
}
|
||||
</style>
|
||||
|
||||
129
app/javascript/dashboard/components/ui/Banner.vue
Normal file
129
app/javascript/dashboard/components/ui/Banner.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<div class="banner" :class="bannerClasses">
|
||||
<span>
|
||||
{{ bannerMessage }}
|
||||
<a
|
||||
v-if="hrefLink"
|
||||
:href="hrefLink"
|
||||
rel="noopener noreferrer nofollow"
|
||||
target="_blank"
|
||||
>
|
||||
{{ hrefLinkText }}
|
||||
</a>
|
||||
</span>
|
||||
<woot-button
|
||||
v-if="hasActionButton"
|
||||
size="small"
|
||||
variant="link"
|
||||
icon="arrow-right"
|
||||
color-scheme="primary"
|
||||
class-names="banner-action__button"
|
||||
@click="onClick"
|
||||
>
|
||||
{{ actionButtonLabel }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-if="hasCloseButton"
|
||||
size="small"
|
||||
variant="link"
|
||||
color-scheme="warning"
|
||||
icon="dismiss-circle"
|
||||
class-names="banner-action__button"
|
||||
@click="onClickClose"
|
||||
>
|
||||
</woot-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
bannerMessage: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
hrefLink: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
hrefLinkText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
hasActionButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
actionButtonLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
colorScheme: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
hasCloseButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
bannerClasses() {
|
||||
return [this.colorScheme];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onClick(e) {
|
||||
this.$emit('click', e);
|
||||
},
|
||||
onClickClose(e) {
|
||||
this.$emit('close', e);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.banner {
|
||||
display: flex;
|
||||
color: var(--white);
|
||||
font-size: var(--font-size-mini);
|
||||
padding: var(--space-slab) var(--space-normal);
|
||||
justify-content: center;
|
||||
position: sticky;
|
||||
|
||||
&.secondary {
|
||||
background: var(--s-300);
|
||||
}
|
||||
|
||||
&.alert {
|
||||
background: var(--r-400);
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background: var(--y-800);
|
||||
color: var(--s-600);
|
||||
a {
|
||||
color: var(--s-600);
|
||||
}
|
||||
}
|
||||
|
||||
&.gray {
|
||||
background: var(--b-500);
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
color: var(--white);
|
||||
font-size: var(--font-size-mini);
|
||||
}
|
||||
|
||||
.banner-action__button {
|
||||
margin: 0 var(--space-smaller);
|
||||
|
||||
::v-deep .button__content {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -30,7 +30,7 @@ export default {
|
||||
},
|
||||
value: {
|
||||
type: Date,
|
||||
default: () => [],
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
>
|
||||
<div class="thumb-wrap">
|
||||
<img
|
||||
v-if="isTypeImage(attachment.resource.type)"
|
||||
v-if="isTypeImage(attachment.resource)"
|
||||
class="image-thumb"
|
||||
:src="attachment.thumb"
|
||||
/>
|
||||
@@ -15,12 +15,12 @@
|
||||
</div>
|
||||
<div class="file-name-wrap">
|
||||
<span class="item">
|
||||
{{ attachment.resource.name }}
|
||||
{{ fileName(attachment.resource) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="file-size-wrap">
|
||||
<span class="item">
|
||||
{{ formatFileSize(attachment.resource.size) }}
|
||||
{{ formatFileSize(attachment.resource) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="remove-file-wrap">
|
||||
@@ -50,12 +50,17 @@ export default {
|
||||
onRemoveAttachment(index) {
|
||||
this.removeAttachment(index);
|
||||
},
|
||||
formatFileSize(size) {
|
||||
formatFileSize(file) {
|
||||
const size = file.byte_size || file.size;
|
||||
return formatBytes(size, 0);
|
||||
},
|
||||
isTypeImage(type) {
|
||||
isTypeImage(file) {
|
||||
const type = file.content_type || file.type;
|
||||
return type.includes('image');
|
||||
},
|
||||
fileName(file) {
|
||||
return file.filename || file.name;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
<template>
|
||||
<div
|
||||
class="filter"
|
||||
:class="{ error: v.action_params.$dirty && v.action_params.$error }"
|
||||
>
|
||||
<div class="filter-inputs">
|
||||
<select
|
||||
v-model="action_name"
|
||||
class="action__question"
|
||||
@change="resetFilter()"
|
||||
>
|
||||
<option
|
||||
v-for="attribute in actionTypes"
|
||||
:key="attribute.key"
|
||||
:value="attribute.key"
|
||||
>
|
||||
{{ attribute.label }}
|
||||
</option>
|
||||
</select>
|
||||
<div class="filter__answer--wrap">
|
||||
<div class="multiselect-wrap--small">
|
||||
<multiselect
|
||||
v-model="action_params"
|
||||
track-by="id"
|
||||
label="name"
|
||||
:placeholder="'Select'"
|
||||
:multiple="true"
|
||||
selected-label
|
||||
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
||||
deselect-label=""
|
||||
:max-height="160"
|
||||
:options="dropdownValues"
|
||||
:allow-empty="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<woot-button
|
||||
icon="dismiss"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
@click="removeAction"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
v-if="v.action_params.$dirty && v.action_params.$error"
|
||||
class="filter-error"
|
||||
>
|
||||
{{ $t('FILTER.EMPTY_VALUE_ERROR') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
actionTypes: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
dropdownValues: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
v: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
action_name: {
|
||||
get() {
|
||||
if (!this.value) return null;
|
||||
return this.value.action_name;
|
||||
},
|
||||
set(value) {
|
||||
const payload = this.value || {};
|
||||
this.$emit('input', { ...payload, action_name: value });
|
||||
},
|
||||
},
|
||||
action_params: {
|
||||
get() {
|
||||
if (!this.value) return null;
|
||||
return this.value.action_params;
|
||||
},
|
||||
set(value) {
|
||||
const payload = this.value || {};
|
||||
this.$emit('input', { ...payload, action_params: value });
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
removeAction() {
|
||||
this.$emit('removeAction');
|
||||
},
|
||||
resetFilter() {
|
||||
this.$emit('resetFilter');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.filter {
|
||||
background: var(--color-background);
|
||||
padding: var(--space-small);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-medium);
|
||||
margin-bottom: var(--space-small);
|
||||
}
|
||||
|
||||
.filter.error {
|
||||
background: var(--r-50);
|
||||
}
|
||||
|
||||
.filter-inputs {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.filter-error {
|
||||
color: var(--r-500);
|
||||
display: block;
|
||||
margin: var(--space-smaller) 0;
|
||||
}
|
||||
|
||||
.action__question,
|
||||
.filter__operator {
|
||||
margin-bottom: var(--space-zero);
|
||||
margin-right: var(--space-smaller);
|
||||
}
|
||||
|
||||
.action__question {
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.filter__answer--wrap {
|
||||
margin-right: var(--space-smaller);
|
||||
flex-grow: 1;
|
||||
|
||||
input {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.filter__answer {
|
||||
&.answer--text-input {
|
||||
margin-bottom: var(--space-zero);
|
||||
}
|
||||
}
|
||||
|
||||
.filter__join-operator-wrap {
|
||||
position: relative;
|
||||
z-index: var(--z-index-twenty);
|
||||
margin: var(--space-zero);
|
||||
}
|
||||
|
||||
.filter__join-operator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
margin: var(--space-one) var(--space-zero);
|
||||
|
||||
.operator__line {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.operator__select {
|
||||
position: relative;
|
||||
width: auto;
|
||||
margin-bottom: var(--space-zero) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect {
|
||||
margin-bottom: var(--space-zero);
|
||||
}
|
||||
</style>
|
||||
@@ -18,11 +18,11 @@ export default {
|
||||
},
|
||||
backgroundColor: {
|
||||
type: String,
|
||||
default: 'white',
|
||||
default: '#c2e1ff',
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '',
|
||||
default: '#1976cc',
|
||||
},
|
||||
customStyle: {
|
||||
type: Object,
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
export const OPERATOR_TYPES_1 = [
|
||||
{
|
||||
value: 'equal_to',
|
||||
label: 'Equal to',
|
||||
},
|
||||
{
|
||||
value: 'not_equal_to',
|
||||
label: 'Not equal to',
|
||||
},
|
||||
];
|
||||
|
||||
export const OPERATOR_TYPES_2 = [
|
||||
{
|
||||
value: 'equal_to',
|
||||
label: 'Equal to',
|
||||
},
|
||||
{
|
||||
value: 'not_equal_to',
|
||||
label: 'Not equal to',
|
||||
},
|
||||
{
|
||||
value: 'is_present',
|
||||
label: 'Is present',
|
||||
},
|
||||
{
|
||||
value: 'is_not_present',
|
||||
label: 'Is not present',
|
||||
},
|
||||
];
|
||||
|
||||
export const OPERATOR_TYPES_3 = [
|
||||
{
|
||||
value: 'equal_to',
|
||||
label: 'Equal to',
|
||||
},
|
||||
{
|
||||
value: 'not_equal_to',
|
||||
label: 'Not equal to',
|
||||
},
|
||||
{
|
||||
value: 'contains',
|
||||
label: 'Contains',
|
||||
},
|
||||
{
|
||||
value: 'does_not_contain',
|
||||
label: 'Does not contain',
|
||||
},
|
||||
];
|
||||
|
||||
export const OPERATOR_TYPES_4 = [
|
||||
{
|
||||
value: 'equal_to',
|
||||
label: 'Equal to',
|
||||
},
|
||||
{
|
||||
value: 'not_equal_to',
|
||||
label: 'Not equal to',
|
||||
},
|
||||
{
|
||||
value: 'is_present',
|
||||
label: 'Is present',
|
||||
},
|
||||
{
|
||||
value: 'is_not_present',
|
||||
label: 'Is not present',
|
||||
},
|
||||
{
|
||||
value: 'is_greater_than',
|
||||
label: 'Is greater than',
|
||||
},
|
||||
{
|
||||
value: 'is_lesser_than',
|
||||
label: 'Is lesser than',
|
||||
},
|
||||
];
|
||||
@@ -1,8 +1,29 @@
|
||||
<template>
|
||||
<div class="filters">
|
||||
<div class="filter">
|
||||
<div class="filter" :class="{ error: v.values.$dirty && v.values.$error }">
|
||||
<div class="filter-inputs">
|
||||
<select
|
||||
v-if="groupedFilters"
|
||||
v-model="attributeKey"
|
||||
class="filter__question"
|
||||
@change="resetFilter()"
|
||||
>
|
||||
<optgroup
|
||||
v-for="(group, i) in filterGroups"
|
||||
:key="i"
|
||||
:label="group.name"
|
||||
>
|
||||
<option
|
||||
v-for="attribute in group.attributes"
|
||||
:key="attribute.key"
|
||||
:value="attribute.key"
|
||||
>
|
||||
{{ attribute.name }}
|
||||
</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<select
|
||||
v-else
|
||||
v-model="attributeKey"
|
||||
class="filter__question"
|
||||
@change="resetFilter()"
|
||||
@@ -63,6 +84,14 @@
|
||||
:option-height="104"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="inputType === 'date'" class="multiselect-wrap--small">
|
||||
<input
|
||||
v-model="values"
|
||||
type="date"
|
||||
:editable="false"
|
||||
class="answer--text-input datepicker"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
v-else
|
||||
v-model="values"
|
||||
@@ -132,6 +161,14 @@ export default {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
groupedFilters: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
filterGroups: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
attributeKey: {
|
||||
@@ -193,6 +230,10 @@ export default {
|
||||
border-radius: var(--border-radius-medium);
|
||||
}
|
||||
|
||||
.filter.error {
|
||||
background: var(--r-50);
|
||||
}
|
||||
|
||||
.filter-inputs {
|
||||
display: flex;
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
<div class="bottom-box" :class="wrapClass">
|
||||
<div class="left-wrap">
|
||||
<woot-button
|
||||
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_EMOJI_ICON')"
|
||||
:title="$t('CONVERSATION.REPLYBOX.TIP_EMOJI_ICON')"
|
||||
icon="emoji"
|
||||
emoji="😊"
|
||||
@@ -14,10 +15,16 @@
|
||||
<!-- ensure the same validations for attachment types are implemented in backend models as well -->
|
||||
<file-upload
|
||||
ref="upload"
|
||||
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_ATTACH_ICON')"
|
||||
:size="4096 * 4096"
|
||||
:accept="allowedFileTypes"
|
||||
:multiple="enableMultipleFileUpload"
|
||||
:drop="true"
|
||||
:drop-directory="false"
|
||||
:data="{
|
||||
direct_upload_url: '/rails/active_storage/direct_uploads',
|
||||
direct_upload: true,
|
||||
}"
|
||||
@input-file="onFileUpload"
|
||||
>
|
||||
<woot-button
|
||||
@@ -33,6 +40,7 @@
|
||||
</file-upload>
|
||||
<woot-button
|
||||
v-if="enableRichEditor && !isOnPrivateNote"
|
||||
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_FORMAT_ICON')"
|
||||
icon="quote"
|
||||
emoji="🖊️"
|
||||
color-scheme="secondary"
|
||||
@@ -41,6 +49,16 @@
|
||||
:title="$t('CONVERSATION.REPLYBOX.TIP_FORMAT_ICON')"
|
||||
@click="toggleFormatMode"
|
||||
/>
|
||||
<woot-button
|
||||
v-if="showMessageSignatureButton"
|
||||
v-tooltip.top-end="signatureToggleTooltip"
|
||||
icon="signature"
|
||||
color-scheme="secondary"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
:title="signatureToggleTooltip"
|
||||
@click="toggleMessageSignature"
|
||||
/>
|
||||
<transition name="modal-fade">
|
||||
<div
|
||||
v-show="$refs.upload && $refs.upload.dropActive"
|
||||
@@ -79,18 +97,22 @@
|
||||
|
||||
<script>
|
||||
import FileUpload from 'vue-upload-component';
|
||||
import * as ActiveStorage from 'activestorage';
|
||||
import {
|
||||
hasPressedAltAndWKey,
|
||||
hasPressedAltAndAKey,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
|
||||
import { ALLOWED_FILE_TYPES } from 'shared/constants/messages';
|
||||
|
||||
import { REPLY_EDITOR_MODES } from './constants';
|
||||
export default {
|
||||
name: 'ReplyTopPanel',
|
||||
components: { FileUpload },
|
||||
mixins: [eventListenerMixins],
|
||||
mixins: [eventListenerMixins, uiSettingsMixin, inboxMixin],
|
||||
props: {
|
||||
mode: {
|
||||
type: String,
|
||||
@@ -104,6 +126,10 @@ export default {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
inbox: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
showFileUpload: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -144,6 +170,10 @@ export default {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
enableMultipleFileUpload: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isNote() {
|
||||
@@ -165,6 +195,21 @@ export default {
|
||||
allowedFileTypes() {
|
||||
return ALLOWED_FILE_TYPES;
|
||||
},
|
||||
showMessageSignatureButton() {
|
||||
return !this.isPrivate && this.isAnEmailChannel;
|
||||
},
|
||||
sendWithSignature() {
|
||||
const { send_with_signature: isEnabled } = this.uiSettings;
|
||||
return isEnabled;
|
||||
},
|
||||
signatureToggleTooltip() {
|
||||
return this.sendWithSignature
|
||||
? this.$t('CONVERSATION.FOOTER.DISABLE_SIGN_TOOLTIP')
|
||||
: this.$t('CONVERSATION.FOOTER.ENABLE_SIGN_TOOLTIP');
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
ActiveStorage.start();
|
||||
},
|
||||
methods: {
|
||||
handleKeyEvents(e) {
|
||||
@@ -181,6 +226,11 @@ export default {
|
||||
toggleEnterToSend() {
|
||||
this.$emit('toggleEnterToSend', !this.enterToSendEnabled);
|
||||
},
|
||||
toggleMessageSignature() {
|
||||
this.updateUISettings({
|
||||
send_with_signature: !this.sendWithSignature,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -9,12 +9,13 @@
|
||||
v-for="(filter, i) in appliedFilters"
|
||||
:key="i"
|
||||
v-model="appliedFilters[i]"
|
||||
:filter-attributes="filterAttributes"
|
||||
:filter-groups="filterGroups"
|
||||
:input-type="getInputType(appliedFilters[i].attribute_key)"
|
||||
:operators="getOperators(appliedFilters[i].attribute_key)"
|
||||
:dropdown-values="getDropdownValues(appliedFilters[i].attribute_key)"
|
||||
:show-query-operator="i !== appliedFilters.length - 1"
|
||||
:show-user-input="showUserInput(appliedFilters[i].filter_operator)"
|
||||
:grouped-filters="true"
|
||||
:v="$v.appliedFilters.$each[i]"
|
||||
@resetFilter="resetFilter(i, appliedFilters[i])"
|
||||
@removeFilter="removeFilter(i)"
|
||||
@@ -48,22 +49,24 @@
|
||||
<script>
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { required, requiredIf } from 'vuelidate/lib/validators';
|
||||
import FilterInputBox from '../FilterInput.vue';
|
||||
import FilterInputBox from '../FilterInput/Index.vue';
|
||||
import languages from './advancedFilterItems/languages';
|
||||
import countries from '/app/javascript/shared/constants/countries.js';
|
||||
import countries from 'shared/constants/countries.js';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import { filterAttributeGroups } from './advancedFilterItems';
|
||||
import filterMixin from 'shared/mixins/filterMixin';
|
||||
import * as OPERATORS from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js';
|
||||
export default {
|
||||
components: {
|
||||
FilterInputBox,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
mixins: [alertMixin, filterMixin],
|
||||
props: {
|
||||
onClose: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
filterTypes: {
|
||||
initialFilterTypes: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
@@ -87,22 +90,21 @@ export default {
|
||||
return {
|
||||
show: true,
|
||||
appliedFilters: [],
|
||||
filterTypes: this.initialFilterTypes,
|
||||
filterAttributeGroups,
|
||||
filterGroups: [],
|
||||
allCustomAttributes: [],
|
||||
attributeModel: 'conversation_attribute',
|
||||
filtersFori18n: 'FILTER',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
filterAttributes() {
|
||||
return this.filterTypes.map(type => {
|
||||
return {
|
||||
key: type.attributeKey,
|
||||
name: this.$t(`FILTER.ATTRIBUTES.${type.attributeI18nKey}`),
|
||||
};
|
||||
});
|
||||
},
|
||||
...mapGetters({
|
||||
getAppliedConversationFilters: 'getAppliedConversationFilters',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
this.setFilterAttributes();
|
||||
this.$store.dispatch('campaigns/get');
|
||||
if (this.getAppliedConversationFilters.length) {
|
||||
this.appliedFilters = [...this.getAppliedConversationFilters];
|
||||
@@ -112,10 +114,41 @@ export default {
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
attribute_model: 'standard',
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getOperatorTypes(key) {
|
||||
switch (key) {
|
||||
case 'list':
|
||||
return OPERATORS.OPERATOR_TYPES_1;
|
||||
case 'text':
|
||||
return OPERATORS.OPERATOR_TYPES_3;
|
||||
case 'number':
|
||||
return OPERATORS.OPERATOR_TYPES_1;
|
||||
case 'link':
|
||||
return OPERATORS.OPERATOR_TYPES_1;
|
||||
case 'date':
|
||||
return OPERATORS.OPERATOR_TYPES_4;
|
||||
case 'checkbox':
|
||||
return OPERATORS.OPERATOR_TYPES_1;
|
||||
default:
|
||||
return OPERATORS.OPERATOR_TYPES_1;
|
||||
}
|
||||
},
|
||||
customAttributeInputType(key) {
|
||||
switch (key) {
|
||||
case 'date':
|
||||
return 'date';
|
||||
default:
|
||||
return 'plain_text';
|
||||
}
|
||||
},
|
||||
getAttributeModel(key) {
|
||||
const type = this.filterTypes.find(filter => filter.attributeKey === key);
|
||||
return type.attributeModel;
|
||||
},
|
||||
getInputType(key) {
|
||||
const type = this.filterTypes.find(filter => filter.attributeKey === key);
|
||||
return type.inputType;
|
||||
@@ -192,7 +225,6 @@ export default {
|
||||
'setConversationFilters',
|
||||
JSON.parse(JSON.stringify(this.appliedFilters))
|
||||
);
|
||||
this.appliedFilters[this.appliedFilters.length - 1].query_operator = null;
|
||||
this.$emit('applyFilter', this.appliedFilters);
|
||||
},
|
||||
resetFilter(index, currentFilter) {
|
||||
|
||||
@@ -130,6 +130,10 @@ export default {
|
||||
type: [String, Number],
|
||||
default: 0,
|
||||
},
|
||||
foldersId: {
|
||||
type: [String, Number],
|
||||
default: 0,
|
||||
},
|
||||
showAssignee: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -248,8 +252,12 @@ export default {
|
||||
id: chat.id,
|
||||
label: this.activeLabel,
|
||||
teamId: this.teamId,
|
||||
foldersId: this.foldersId,
|
||||
conversationType: this.conversationType,
|
||||
});
|
||||
if (this.isActiveChat) {
|
||||
return;
|
||||
}
|
||||
router.push({ path: frontendURL(path) });
|
||||
},
|
||||
},
|
||||
|
||||
@@ -57,22 +57,26 @@
|
||||
/>
|
||||
</div>
|
||||
<spinner v-if="isPending" size="tiny" />
|
||||
<a
|
||||
v-if="isATweet && isIncoming && sender"
|
||||
<div
|
||||
v-if="showAvatar"
|
||||
v-tooltip.top="tooltipForSender"
|
||||
class="sender--info"
|
||||
:href="twitterProfileLink"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
>
|
||||
<woot-thumbnail
|
||||
:src="sender.thumbnail"
|
||||
:username="sender.name"
|
||||
:username="senderNameForAvatar"
|
||||
size="16px"
|
||||
/>
|
||||
<div class="sender--available-name">
|
||||
<a
|
||||
v-if="isATweet && isIncoming"
|
||||
class="sender--available-name"
|
||||
:href="twitterProfileLink"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
>
|
||||
{{ sender.name }}
|
||||
</div>
|
||||
</a>
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="isFailed" class="message-failed--alert">
|
||||
<woot-button
|
||||
v-tooltip.top-end="$t('CONVERSATION.TRY_AGAIN')"
|
||||
@@ -113,7 +117,6 @@ import BubbleActions from './bubble/Actions';
|
||||
import Spinner from 'shared/components/Spinner';
|
||||
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu';
|
||||
|
||||
import { isEmptyObject } from 'dashboard/helper/commons';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import contentTypeMixin from 'shared/mixins/contentTypeMixin';
|
||||
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
|
||||
@@ -242,6 +245,12 @@ export default {
|
||||
isIncoming() {
|
||||
return this.data.message_type === MESSAGE_TYPE.INCOMING;
|
||||
},
|
||||
isOutgoing() {
|
||||
return this.data.message_type === MESSAGE_TYPE.OUTGOING;
|
||||
},
|
||||
isTemplate() {
|
||||
return this.data.message_type === MESSAGE_TYPE.TEMPLATE;
|
||||
},
|
||||
emailHeadAttributes() {
|
||||
return {
|
||||
email: this.contentAttributes.email,
|
||||
@@ -258,6 +267,19 @@ export default {
|
||||
hasText() {
|
||||
return !!this.data.content;
|
||||
},
|
||||
tooltipForSender() {
|
||||
const name = this.senderNameForAvatar;
|
||||
const { message_type: messageType } = this.data;
|
||||
const showTooltip =
|
||||
messageType === MESSAGE_TYPE.OUTGOING ||
|
||||
messageType === MESSAGE_TYPE.TEMPLATE;
|
||||
return showTooltip
|
||||
? {
|
||||
content: `${this.$t('CONVERSATION.SENT_BY')} ${name}`,
|
||||
classes: 'top',
|
||||
}
|
||||
: false;
|
||||
},
|
||||
messageToolTip() {
|
||||
if (this.isMessageDeleted) {
|
||||
return false;
|
||||
@@ -265,13 +287,7 @@ export default {
|
||||
if (this.isFailed) {
|
||||
return this.$t(`CONVERSATION.SEND_FAILED`);
|
||||
}
|
||||
const { sender } = this;
|
||||
return this.data.message_type === 1 && !isEmptyObject(sender)
|
||||
? {
|
||||
content: `${this.$t('CONVERSATION.SENT_BY')} ${sender.name}`,
|
||||
classes: 'top',
|
||||
}
|
||||
: false;
|
||||
return false;
|
||||
},
|
||||
wrapClass() {
|
||||
return {
|
||||
@@ -313,6 +329,19 @@ export default {
|
||||
const { meta } = this.data;
|
||||
return meta ? meta.error : '';
|
||||
},
|
||||
showAvatar() {
|
||||
if (this.isOutgoing || this.isTemplate) {
|
||||
return true;
|
||||
}
|
||||
return this.isATweet && this.isIncoming && this.sender;
|
||||
},
|
||||
senderNameForAvatar() {
|
||||
if (this.isOutgoing || this.isTemplate) {
|
||||
const { name = this.$t('CONVERSATION.BOT') } = this.sender || {};
|
||||
return name;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
data() {
|
||||
@@ -423,6 +452,9 @@ export default {
|
||||
.message-text--metadata .time {
|
||||
color: var(--v-50);
|
||||
}
|
||||
&.is-private .message-text--metadata .time {
|
||||
color: var(--s-400);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-failed {
|
||||
|
||||
@@ -1,56 +1,29 @@
|
||||
<template>
|
||||
<div class="view-box fill-height">
|
||||
<div
|
||||
<banner
|
||||
v-if="!currentChat.can_reply && !isAWhatsappChannel"
|
||||
class="banner messenger-policy--banner"
|
||||
>
|
||||
<span>
|
||||
{{ $t('CONVERSATION.CANNOT_REPLY') }}
|
||||
<a
|
||||
:href="facebookReplyPolicy"
|
||||
rel="noopener noreferrer nofollow"
|
||||
target="_blank"
|
||||
>
|
||||
{{ $t('CONVERSATION.24_HOURS_WINDOW') }}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="!currentChat.can_reply && isAWhatsappChannel"
|
||||
class="banner messenger-policy--banner"
|
||||
>
|
||||
<span>
|
||||
{{ $t('CONVERSATION.TWILIO_WHATSAPP_CAN_REPLY') }}
|
||||
<a
|
||||
:href="twilioWhatsAppReplyPolicy"
|
||||
rel="noopener noreferrer nofollow"
|
||||
target="_blank"
|
||||
>
|
||||
{{ $t('CONVERSATION.TWILIO_WHATSAPP_24_HOURS_WINDOW') }}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
color-scheme="alert"
|
||||
:banner-message="$t('CONVERSATION.CANNOT_REPLY')"
|
||||
:href-link="facebookReplyPolicy"
|
||||
:href-link-text="$t('CONVERSATION.24_HOURS_WINDOW')"
|
||||
/>
|
||||
|
||||
<banner
|
||||
v-if="!currentChat.can_reply && isAWhatsappChannel"
|
||||
color-scheme="alert"
|
||||
:banner-message="$t('CONVERSATION.TWILIO_WHATSAPP_CAN_REPLY')"
|
||||
:href-link="twilioWhatsAppReplyPolicy"
|
||||
:href-link-text="$t('CONVERSATION.TWILIO_WHATSAPP_24_HOURS_WINDOW')"
|
||||
/>
|
||||
|
||||
<banner
|
||||
v-if="isATweet"
|
||||
color-scheme="gray"
|
||||
:banner-message="tweetBannerText"
|
||||
:has-close-button="hasSelectedTweetId"
|
||||
@close="removeTweetSelection"
|
||||
/>
|
||||
|
||||
<div v-if="isATweet" class="banner">
|
||||
<span v-if="!selectedTweetId">
|
||||
{{ $t('CONVERSATION.SELECT_A_TWEET_TO_REPLY') }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t('CONVERSATION.REPLYING_TO') }}
|
||||
{{ selectedTweet.content || '' }}
|
||||
</span>
|
||||
<button
|
||||
v-if="selectedTweetId"
|
||||
class="banner-close-button"
|
||||
@click="removeTweetSelection"
|
||||
>
|
||||
<fluent-icon
|
||||
v-tooltip="$t('CONVERSATION.REMOVE_SELECTION')"
|
||||
size="16"
|
||||
icon="dismiss"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="sidebar-toggle__wrap">
|
||||
<woot-button
|
||||
variant="smooth"
|
||||
@@ -126,6 +99,7 @@ import { mapGetters } from 'vuex';
|
||||
import ReplyBox from './ReplyBox';
|
||||
import Message from './Message';
|
||||
import conversationMixin from '../../../mixins/conversations';
|
||||
import Banner from 'dashboard/components/ui/Banner.vue';
|
||||
import { getTypingUsersText } from '../../../helper/commons';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import { REPLY_POLICY } from 'shared/constants/links';
|
||||
@@ -139,6 +113,7 @@ export default {
|
||||
components: {
|
||||
Message,
|
||||
ReplyBox,
|
||||
Banner,
|
||||
},
|
||||
mixins: [conversationMixin, inboxMixin, eventListenerMixins, clickaway],
|
||||
props: {
|
||||
@@ -173,7 +148,17 @@ export default {
|
||||
inbox() {
|
||||
return this.$store.getters['inboxes/getInbox'](this.inboxId);
|
||||
},
|
||||
hasSelectedTweetId() {
|
||||
return !!this.selectedTweetId;
|
||||
},
|
||||
|
||||
tweetBannerText() {
|
||||
return !this.selectedTweetId
|
||||
? this.$t('CONVERSATION.SELECT_A_TWEET_TO_REPLY')
|
||||
: `
|
||||
${this.$t('CONVERSATION.REPLYING_TO')}
|
||||
${this.selectedTweet.content}` || '';
|
||||
},
|
||||
typingUsersList() {
|
||||
const userList = this.$store.getters[
|
||||
'conversationTypingStatus/getUserList'
|
||||
@@ -375,31 +360,6 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.banner {
|
||||
background: var(--b-500);
|
||||
color: var(--white);
|
||||
font-size: var(--font-size-mini);
|
||||
padding: var(--space-slab) var(--space-normal);
|
||||
text-align: center;
|
||||
position: relative;
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
color: var(--white);
|
||||
font-size: var(--font-size-mini);
|
||||
}
|
||||
|
||||
&.messenger-policy--banner {
|
||||
background: var(--r-400);
|
||||
}
|
||||
|
||||
.banner-close-button {
|
||||
cursor: pointer;
|
||||
margin-left: var(--space--two);
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
.spinner--container {
|
||||
min-height: var(--space-jumbo);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
<template>
|
||||
<div class="reply-box" :class="replyBoxClass">
|
||||
<banner
|
||||
v-if="showSelfAssignBanner"
|
||||
color-scheme="secondary"
|
||||
:banner-message="$t('CONVERSATION.NOT_ASSIGNED_TO_YOU')"
|
||||
:has-action-button="true"
|
||||
:action-button-label="$t('CONVERSATION.ASSIGN_TO_ME')"
|
||||
@click="onClickSelfAssign"
|
||||
/>
|
||||
<reply-top-panel
|
||||
:mode="replyType"
|
||||
:set-reply-mode="setReplyMode"
|
||||
@@ -58,8 +66,16 @@
|
||||
:remove-attachment="removeAttachment"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="showMessageSignature"
|
||||
v-tooltip="$t('CONVERSATION.FOOTER.MESSAGE_SIGN_TOOLTIP')"
|
||||
class="message-signature-wrap"
|
||||
>
|
||||
<p class="message-signature" v-html="formatMessage(messageSignature)" />
|
||||
</div>
|
||||
<reply-bottom-panel
|
||||
:mode="replyType"
|
||||
:inbox="inbox"
|
||||
:send-button-text="replyButtonLabel"
|
||||
:on-file-upload="onFileUpload"
|
||||
:show-file-upload="showFileUpload"
|
||||
@@ -72,6 +88,7 @@
|
||||
:is-format-mode="showRichContentEditor"
|
||||
:enable-rich-editor="isRichEditorEnabled"
|
||||
:enter-to-send-enabled="enterToSendEnabled"
|
||||
:enable-multiple-file-upload="enableMultipleFileUpload"
|
||||
@toggleEnterToSend="toggleEnterToSend"
|
||||
/>
|
||||
</div>
|
||||
@@ -89,8 +106,10 @@ import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview';
|
||||
import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel';
|
||||
import ReplyEmailHead from './ReplyEmailHead';
|
||||
import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel';
|
||||
import Banner from 'dashboard/components/ui/Banner.vue';
|
||||
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
|
||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
||||
import { MAXIMUM_FILE_UPLOAD_SIZE } from 'shared/constants/messages';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
@@ -99,10 +118,12 @@ import {
|
||||
isEscape,
|
||||
isEnter,
|
||||
hasPressedShift,
|
||||
hasPressedCommandPlusKKey,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import { DirectUpload } from 'activestorage';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -114,8 +135,15 @@ export default {
|
||||
ReplyEmailHead,
|
||||
ReplyBottomPanel,
|
||||
WootMessageEditor,
|
||||
Banner,
|
||||
},
|
||||
mixins: [clickaway, inboxMixin, uiSettingsMixin, alertMixin],
|
||||
mixins: [
|
||||
clickaway,
|
||||
inboxMixin,
|
||||
uiSettingsMixin,
|
||||
alertMixin,
|
||||
messageFormatterMixin,
|
||||
],
|
||||
props: {
|
||||
selectedTweet: {
|
||||
type: [Object, String],
|
||||
@@ -147,6 +175,13 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentChat: 'getSelectedChat',
|
||||
messageSignature: 'getMessageSignature',
|
||||
currentUser: 'getCurrentUser',
|
||||
globalConfig: 'globalConfig/get',
|
||||
}),
|
||||
|
||||
showRichContentEditor() {
|
||||
if (this.isOnPrivateNote) {
|
||||
return true;
|
||||
@@ -161,7 +196,36 @@ export default {
|
||||
}
|
||||
return false;
|
||||
},
|
||||
...mapGetters({ currentChat: 'getSelectedChat' }),
|
||||
assignedAgent: {
|
||||
get() {
|
||||
return this.currentChat.meta.assignee;
|
||||
},
|
||||
set(agent) {
|
||||
const agentId = agent ? agent.id : 0;
|
||||
this.$store.dispatch('setCurrentChatAssignee', agent);
|
||||
this.$store
|
||||
.dispatch('assignAgent', {
|
||||
conversationId: this.currentChat.id,
|
||||
agentId,
|
||||
})
|
||||
.then(() => {
|
||||
this.showAlert(this.$t('CONVERSATION.CHANGE_AGENT'));
|
||||
});
|
||||
},
|
||||
},
|
||||
showSelfAssignBanner() {
|
||||
if (this.message !== '' && !this.isOnPrivateNote) {
|
||||
if (!this.assignedAgent) {
|
||||
return true;
|
||||
}
|
||||
if (this.assignedAgent.id !== this.currentUser.id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
enterToSendEnabled() {
|
||||
return !!this.uiSettings.enter_to_send_enabled;
|
||||
},
|
||||
@@ -189,7 +253,7 @@ export default {
|
||||
return this.maxLength - this.message.length;
|
||||
},
|
||||
isReplyButtonDisabled() {
|
||||
if (this.isATweet && !this.inReplyTo) {
|
||||
if (this.isATweet && !this.inReplyTo && !this.isOnPrivateNote) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -283,6 +347,16 @@ export default {
|
||||
showReplyHead() {
|
||||
return !this.isOnPrivateNote && this.isAnEmailChannel;
|
||||
},
|
||||
enableMultipleFileUpload() {
|
||||
return this.isAnEmailChannel || this.isAWebWidgetInbox || this.isAPIInbox;
|
||||
},
|
||||
showMessageSignature() {
|
||||
return !this.isPrivate && this.isAnEmailChannel && this.sendWithSignature;
|
||||
},
|
||||
sendWithSignature() {
|
||||
const { send_with_signature: isEnabled } = this.uiSettings;
|
||||
return isEnabled;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentChat(conversation) {
|
||||
@@ -355,17 +429,49 @@ export default {
|
||||
e.preventDefault();
|
||||
this.sendMessage();
|
||||
}
|
||||
} else if (hasPressedCommandPlusKKey(e)) {
|
||||
this.openCommandBar();
|
||||
}
|
||||
},
|
||||
openCommandBar() {
|
||||
const ninja = document.querySelector('ninja-keys');
|
||||
ninja.open();
|
||||
},
|
||||
toggleEnterToSend(enterToSendEnabled) {
|
||||
this.updateUISettings({ enter_to_send_enabled: enterToSendEnabled });
|
||||
},
|
||||
onClickSelfAssign() {
|
||||
const {
|
||||
account_id,
|
||||
availability_status,
|
||||
available_name,
|
||||
email,
|
||||
id,
|
||||
name,
|
||||
role,
|
||||
avatar_url,
|
||||
} = this.currentUser;
|
||||
const selfAssign = {
|
||||
account_id,
|
||||
availability_status,
|
||||
available_name,
|
||||
email,
|
||||
id,
|
||||
name,
|
||||
role,
|
||||
thumbnail: avatar_url,
|
||||
};
|
||||
this.assignedAgent = selfAssign;
|
||||
},
|
||||
async sendMessage() {
|
||||
if (this.isReplyButtonDisabled) {
|
||||
return;
|
||||
}
|
||||
if (!this.showMentions) {
|
||||
const newMessage = this.message;
|
||||
let newMessage = this.message;
|
||||
if (this.sendWithSignature && this.messageSignature) {
|
||||
newMessage += '\n\n' + this.messageSignature;
|
||||
}
|
||||
const messagePayload = this.getMessagePayload(newMessage);
|
||||
this.clearMessage();
|
||||
try {
|
||||
@@ -440,21 +546,30 @@ export default {
|
||||
});
|
||||
},
|
||||
onFileUpload(file) {
|
||||
if (this.globalConfig.directUploadsEnabled) {
|
||||
this.onDirectFileUpload(file);
|
||||
} else {
|
||||
this.onIndirectFileUpload(file);
|
||||
}
|
||||
},
|
||||
onDirectFileUpload(file) {
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) {
|
||||
this.attachedFiles = [];
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file.file);
|
||||
reader.onloadend = () => {
|
||||
this.attachedFiles.push({
|
||||
currentChatId: this.currentChat.id,
|
||||
resource: file,
|
||||
isPrivate: this.isPrivate,
|
||||
thumb: reader.result,
|
||||
});
|
||||
};
|
||||
const upload = new DirectUpload(
|
||||
file.file,
|
||||
'/rails/active_storage/direct_uploads',
|
||||
null,
|
||||
file.file.name
|
||||
);
|
||||
upload.create((error, blob) => {
|
||||
if (error) {
|
||||
this.showAlert(error);
|
||||
} else {
|
||||
this.attachFile({ file, blob });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.showAlert(
|
||||
this.$t('CONVERSATION.FILE_SIZE_LIMIT', {
|
||||
@@ -463,13 +578,39 @@ export default {
|
||||
);
|
||||
}
|
||||
},
|
||||
onIndirectFileUpload(file) {
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) {
|
||||
this.attachFile({ file });
|
||||
} else {
|
||||
this.showAlert(
|
||||
this.$t('CONVERSATION.FILE_SIZE_LIMIT', {
|
||||
MAXIMUM_FILE_UPLOAD_SIZE,
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
attachFile({ blob, file }) {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file.file);
|
||||
reader.onloadend = () => {
|
||||
this.attachedFiles.push({
|
||||
currentChatId: this.currentChat.id,
|
||||
resource: blob || file,
|
||||
isPrivate: this.isPrivate,
|
||||
thumb: reader.result,
|
||||
blobSignedId: blob ? blob.signed_id : undefined,
|
||||
});
|
||||
};
|
||||
},
|
||||
removeAttachment(itemIndex) {
|
||||
this.attachedFiles = this.attachedFiles.filter(
|
||||
(item, index) => itemIndex !== index
|
||||
);
|
||||
},
|
||||
getMessagePayload(message) {
|
||||
const [attachment] = this.attachedFiles;
|
||||
const messagePayload = {
|
||||
conversationId: this.currentChat.id,
|
||||
message,
|
||||
@@ -480,8 +621,15 @@ export default {
|
||||
messagePayload.contentAttributes = { in_reply_to: this.inReplyTo };
|
||||
}
|
||||
|
||||
if (attachment) {
|
||||
messagePayload.file = attachment.resource.file;
|
||||
if (this.attachedFiles && this.attachedFiles.length) {
|
||||
messagePayload.files = [];
|
||||
this.attachedFiles.forEach(attachment => {
|
||||
if (this.globalConfig.directUploadsEnabled) {
|
||||
messagePayload.files.push(attachment.blobSignedId);
|
||||
} else {
|
||||
messagePayload.files.push(attachment.resource.file);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this.ccEmails) {
|
||||
@@ -510,6 +658,25 @@ export default {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.message-signature-wrap {
|
||||
margin: 0 var(--space-normal);
|
||||
padding: var(--space-small);
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
border: 1px dashed var(--s-100);
|
||||
border-radius: var(--border-radius-small);
|
||||
|
||||
&:hover {
|
||||
background: var(--s-25);
|
||||
}
|
||||
}
|
||||
|
||||
.message-signature {
|
||||
width: fit-content;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.attachment-preview-box {
|
||||
padding: 0 var(--space-normal);
|
||||
background: transparent;
|
||||
|
||||
@@ -1,229 +1,144 @@
|
||||
import {
|
||||
OPERATOR_TYPES_1,
|
||||
OPERATOR_TYPES_2,
|
||||
OPERATOR_TYPES_3,
|
||||
} from '../../FilterInput/FilterOperatorTypes';
|
||||
|
||||
const filterTypes = [
|
||||
{
|
||||
attributeKey: 'status',
|
||||
attributeI18nKey: 'STATUS',
|
||||
inputType: 'multi_select',
|
||||
dataType: 'text',
|
||||
filterOperators: [
|
||||
{
|
||||
value: 'equal_to',
|
||||
label: 'Equal to',
|
||||
},
|
||||
{
|
||||
value: 'not_equal_to',
|
||||
label: 'Not equal to',
|
||||
},
|
||||
],
|
||||
attribute_type: 'standard',
|
||||
filterOperators: OPERATOR_TYPES_1,
|
||||
attributeModel: 'standard',
|
||||
},
|
||||
{
|
||||
attributeKey: 'assignee_id',
|
||||
attributeI18nKey: 'ASSIGNEE_NAME',
|
||||
inputType: 'search_select',
|
||||
dataType: 'text',
|
||||
filterOperators: [
|
||||
{
|
||||
value: 'equal_to',
|
||||
label: 'Equal to',
|
||||
},
|
||||
{
|
||||
value: 'not_equal_to',
|
||||
label: 'Not equal to',
|
||||
},
|
||||
{
|
||||
value: 'is_present',
|
||||
label: 'Is present',
|
||||
},
|
||||
{
|
||||
value: 'is_not_present',
|
||||
label: 'Is not present',
|
||||
},
|
||||
],
|
||||
attribute_type: 'standard',
|
||||
filterOperators: OPERATOR_TYPES_2,
|
||||
attributeModel: 'standard',
|
||||
},
|
||||
{
|
||||
attributeKey: 'inbox_id',
|
||||
attributeI18nKey: 'INBOX_NAME',
|
||||
inputType: 'search_select',
|
||||
dataType: 'text',
|
||||
filterOperators: [
|
||||
{
|
||||
value: 'equal_to',
|
||||
label: 'Equal to',
|
||||
},
|
||||
{
|
||||
value: 'not_equal_to',
|
||||
label: 'Not equal to',
|
||||
},
|
||||
{
|
||||
value: 'is_present',
|
||||
label: 'Is present',
|
||||
},
|
||||
{
|
||||
value: 'is_not_present',
|
||||
label: 'Is not present',
|
||||
},
|
||||
],
|
||||
attribute_type: 'standard',
|
||||
filterOperators: OPERATOR_TYPES_2,
|
||||
attributeModel: 'standard',
|
||||
},
|
||||
{
|
||||
attributeKey: 'team_id',
|
||||
attributeI18nKey: 'TEAM_NAME',
|
||||
inputType: 'search_select',
|
||||
dataType: 'number',
|
||||
filterOperators: [
|
||||
{
|
||||
value: 'equal_to',
|
||||
label: 'Equal to',
|
||||
},
|
||||
{
|
||||
value: 'not_equal_to',
|
||||
label: 'Not equal to',
|
||||
},
|
||||
{
|
||||
value: 'is_present',
|
||||
label: 'Is present',
|
||||
},
|
||||
{
|
||||
value: 'is_not_present',
|
||||
label: 'Is not present',
|
||||
},
|
||||
],
|
||||
attribute_type: 'standard',
|
||||
filterOperators: OPERATOR_TYPES_2,
|
||||
attributeModel: 'standard',
|
||||
},
|
||||
{
|
||||
attributeKey: 'display_id',
|
||||
attributeI18nKey: 'CONVERSATION_IDENTIFIER',
|
||||
inputType: 'plain_text',
|
||||
dataType: 'Number',
|
||||
filterOperators: [
|
||||
{
|
||||
value: 'equal_to',
|
||||
label: 'Equal to',
|
||||
},
|
||||
{
|
||||
value: 'not_equal_to',
|
||||
label: 'Not equal to',
|
||||
},
|
||||
{
|
||||
value: 'contains',
|
||||
label: 'Contains',
|
||||
},
|
||||
{
|
||||
value: 'does_not_contain',
|
||||
label: 'Does not contain',
|
||||
},
|
||||
],
|
||||
attribute_type: 'standard',
|
||||
filterOperators: OPERATOR_TYPES_3,
|
||||
attributeModel: 'standard',
|
||||
},
|
||||
{
|
||||
attributeKey: 'campaign_id',
|
||||
attributeI18nKey: 'CAMPAIGN_NAME',
|
||||
inputType: 'search_select',
|
||||
dataType: 'Number',
|
||||
filterOperators: [
|
||||
{
|
||||
value: 'equal_to',
|
||||
label: 'Equal to',
|
||||
},
|
||||
{
|
||||
value: 'not_equal_to',
|
||||
label: 'Not equal to',
|
||||
},
|
||||
{
|
||||
value: 'is_present',
|
||||
label: 'Is present',
|
||||
},
|
||||
{
|
||||
value: 'is_not_present',
|
||||
label: 'Is not present',
|
||||
},
|
||||
],
|
||||
attribute_type: 'standard',
|
||||
filterOperators: OPERATOR_TYPES_2,
|
||||
attributeModel: 'standard',
|
||||
},
|
||||
{
|
||||
attributeKey: 'labels',
|
||||
attributeI18nKey: 'LABELS',
|
||||
inputType: 'multi_select',
|
||||
dataType: 'text',
|
||||
filterOperators: [
|
||||
{
|
||||
value: 'equal_to',
|
||||
label: 'Equal to',
|
||||
},
|
||||
{
|
||||
value: 'not_equal_to',
|
||||
label: 'Not equal to',
|
||||
},
|
||||
{
|
||||
value: 'is_present',
|
||||
label: 'Is present',
|
||||
},
|
||||
{
|
||||
value: 'is_not_present',
|
||||
label: 'Is not present',
|
||||
},
|
||||
],
|
||||
attribute_type: 'standard',
|
||||
filterOperators: OPERATOR_TYPES_2,
|
||||
attributeModel: 'standard',
|
||||
},
|
||||
{
|
||||
attributeKey: 'browser_language',
|
||||
attributeI18nKey: 'BROWSER_LANGUAGE',
|
||||
inputType: 'search_select',
|
||||
dataType: 'text',
|
||||
filterOperators: [
|
||||
{
|
||||
value: 'equal_to',
|
||||
label: 'Equal to',
|
||||
},
|
||||
{
|
||||
value: 'not_equal_to',
|
||||
label: 'Not equal to',
|
||||
},
|
||||
],
|
||||
attribute_type: 'additional_attributes',
|
||||
filterOperators: OPERATOR_TYPES_1,
|
||||
attributeModel: 'additional',
|
||||
},
|
||||
{
|
||||
attributeKey: 'country_code',
|
||||
attributeI18nKey: 'COUNTRY_NAME',
|
||||
inputType: 'search_select',
|
||||
dataType: 'text',
|
||||
filterOperators: [
|
||||
{
|
||||
value: 'equal_to',
|
||||
label: 'Equal to',
|
||||
},
|
||||
{
|
||||
value: 'not_equal_to',
|
||||
label: 'Not equal to',
|
||||
},
|
||||
],
|
||||
attribute_type: 'additional_attributes',
|
||||
filterOperators: OPERATOR_TYPES_1,
|
||||
attributeModel: 'additional',
|
||||
},
|
||||
{
|
||||
attributeKey: 'referer',
|
||||
attributeI18nKey: 'REFERER_LINK',
|
||||
inputType: 'plain_text',
|
||||
dataType: 'text',
|
||||
filterOperators: [
|
||||
filterOperators: OPERATOR_TYPES_3,
|
||||
attributeModel: 'additional',
|
||||
},
|
||||
];
|
||||
|
||||
export const filterAttributeGroups = [
|
||||
{
|
||||
name: 'Standard Filters',
|
||||
i18nGroup: 'STANDARD_FILTERS',
|
||||
attributes: [
|
||||
{
|
||||
value: 'equal_to',
|
||||
label: 'Equal to',
|
||||
key: 'status',
|
||||
i18nKey: 'STATUS',
|
||||
},
|
||||
{
|
||||
value: 'not_equal_to',
|
||||
label: 'Not equal to',
|
||||
key: 'assignee_id',
|
||||
i18nKey: 'ASSIGNEE_NAME',
|
||||
},
|
||||
{
|
||||
value: 'contains',
|
||||
label: 'Contains',
|
||||
key: 'inbox_id',
|
||||
i18nKey: 'INBOX_NAME',
|
||||
},
|
||||
{
|
||||
value: 'does_not_contain',
|
||||
label: 'Does not contain',
|
||||
key: 'team_id',
|
||||
i18nKey: 'TEAM_NAME',
|
||||
},
|
||||
{
|
||||
key: 'display_id',
|
||||
i18nKey: 'CONVERSATION_IDENTIFIER',
|
||||
},
|
||||
{
|
||||
key: 'campaign_id',
|
||||
i18nKey: 'CAMPAIGN_NAME',
|
||||
},
|
||||
{
|
||||
key: 'labels',
|
||||
i18nKey: 'LABELS',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Additional Filters',
|
||||
i18nGroup: 'ADDITIONAL_FILTERS',
|
||||
attributes: [
|
||||
{
|
||||
key: 'browser_language',
|
||||
i18nKey: 'BROWSER_LANGUAGE',
|
||||
},
|
||||
{
|
||||
key: 'country_code',
|
||||
i18nKey: 'COUNTRY_NAME',
|
||||
},
|
||||
{
|
||||
key: 'referer',
|
||||
i18nKey: 'REFERER_LINK',
|
||||
},
|
||||
],
|
||||
attribute_type: 'additional_attributes',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -137,10 +137,10 @@ export default {
|
||||
.time {
|
||||
color: var(--w-100);
|
||||
}
|
||||
}
|
||||
|
||||
.action--icon {
|
||||
color: var(--white);
|
||||
.action--icon {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
class="message__mail-head"
|
||||
:class="{ 'is-incoming': isIncoming }"
|
||||
>
|
||||
<div v-if="fromMail" class="meta-wrap">
|
||||
<span class="message__content--type">{{ $t('EMAIL_HEADER.FROM') }}:</span>
|
||||
<span>{{ fromMail }}</span>
|
||||
</div>
|
||||
<div v-if="toMails" class="meta-wrap">
|
||||
<span class="message__content--type">{{ $t('EMAIL_HEADER.TO') }}:</span>
|
||||
<span>{{ toMails }}</span>
|
||||
@@ -46,6 +50,10 @@ export default {
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
fromMail() {
|
||||
const from = this.emailAttributes.from || [];
|
||||
return from.join(', ');
|
||||
},
|
||||
toMails() {
|
||||
const to = this.emailAttributes.to || [];
|
||||
return to.join(', ');
|
||||
|
||||
@@ -84,7 +84,8 @@ describe('MoveActions', () => {
|
||||
|
||||
expect(window.bus.$emit).toBeCalledWith(
|
||||
'newToastMessage',
|
||||
'This conversation is muted for 6 hours'
|
||||
'This conversation is muted for 6 hours',
|
||||
undefined
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -109,7 +110,8 @@ describe('MoveActions', () => {
|
||||
|
||||
expect(window.bus.$emit).toBeCalledWith(
|
||||
'newToastMessage',
|
||||
'This conversation is unmuted'
|
||||
'This conversation is unmuted',
|
||||
undefined
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ export const conversationUrl = ({
|
||||
label,
|
||||
teamId,
|
||||
conversationType = '',
|
||||
foldersId,
|
||||
}) => {
|
||||
let url = `accounts/${accountId}/conversations/${id}`;
|
||||
if (activeInbox) {
|
||||
@@ -20,6 +21,8 @@ export const conversationUrl = ({
|
||||
url = `accounts/${accountId}/label/${label}/conversations/${id}`;
|
||||
} else if (teamId) {
|
||||
url = `accounts/${accountId}/team/${teamId}/conversations/${id}`;
|
||||
} else if (foldersId && foldersId !== 0) {
|
||||
url = `accounts/${accountId}/custom_view/${foldersId}/conversations/${id}`;
|
||||
} else if (conversationType === 'mention') {
|
||||
url = `accounts/${accountId}/mentions/conversations/${id}`;
|
||||
}
|
||||
@@ -34,3 +37,9 @@ export const accountIdFromPathname = pathname => {
|
||||
const accountId = isScoped ? Number(urlParam) : '';
|
||||
return accountId;
|
||||
};
|
||||
|
||||
export const isValidURL = value => {
|
||||
/* eslint-disable no-useless-escape */
|
||||
const URL_REGEX = /^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);
|
||||
};
|
||||
|
||||
18
app/javascript/dashboard/helper/actionQueryGenerator.js
Normal file
18
app/javascript/dashboard/helper/actionQueryGenerator.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const generatePayload = data => {
|
||||
const actions = JSON.parse(JSON.stringify(data));
|
||||
let payload = actions.map(item => {
|
||||
if (Array.isArray(item.action_params)) {
|
||||
item.action_params = item.action_params.map(val => val.id);
|
||||
} else if (typeof item.values === 'object') {
|
||||
item.action_params = [item.action_params.id];
|
||||
} else if (!item.action_params) {
|
||||
item.action_params = [];
|
||||
} else {
|
||||
item.action_params = [item.action_params];
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return payload;
|
||||
};
|
||||
|
||||
export default generatePayload;
|
||||
@@ -1,5 +1,19 @@
|
||||
const lowerCaseValues = (operator, values) => {
|
||||
if (operator === 'equal_to' || operator === 'not_equal_to') {
|
||||
values = values.map(val => {
|
||||
if (typeof val === 'string') {
|
||||
return val.toLowerCase();
|
||||
}
|
||||
return val;
|
||||
});
|
||||
}
|
||||
return values;
|
||||
};
|
||||
|
||||
const generatePayload = data => {
|
||||
let payload = data.map(item => {
|
||||
// Make a copy of data to avoid vue data reactivity issues
|
||||
const filters = JSON.parse(JSON.stringify(data));
|
||||
let payload = filters.map(item => {
|
||||
if (Array.isArray(item.values)) {
|
||||
item.values = item.values.map(val => val.id);
|
||||
} else if (typeof item.values === 'object') {
|
||||
@@ -9,9 +23,14 @@ const generatePayload = data => {
|
||||
} else {
|
||||
item.values = [item.values];
|
||||
}
|
||||
// Convert all values to lowerCase if operator_type is 'equal_to' or 'not_equal_to'
|
||||
item.values = lowerCaseValues(item.filter_operator, item.values);
|
||||
return item;
|
||||
});
|
||||
|
||||
// For every query added, the query_operator is set default to and so the
|
||||
// last query will have an extra query_operator, this would break the api.
|
||||
// Setting this to null for all query payload
|
||||
payload[payload.length - 1].query_operator = undefined;
|
||||
return { payload };
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
frontendURL,
|
||||
conversationUrl,
|
||||
accountIdFromPathname,
|
||||
isValidURL,
|
||||
} from '../URLHelper';
|
||||
|
||||
describe('#URL Helpers', () => {
|
||||
@@ -48,4 +49,13 @@ describe('#URL Helpers', () => {
|
||||
expect(accountIdFromPathname('')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValidURL', () => {
|
||||
it('should return true if valid url is passed', () => {
|
||||
expect(isValidURL('https://chatwoot.com')).toBe(true);
|
||||
});
|
||||
it('should return false if invalid url is passed', () => {
|
||||
expect(isValidURL('alert.window')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import actionQueryGenerator from '../actionQueryGenerator';
|
||||
|
||||
const testData = [
|
||||
{
|
||||
action_name: 'add_label',
|
||||
action_params: [{ id: 'testlabel', name: 'testlabel' }],
|
||||
},
|
||||
{
|
||||
action_name: 'assign_team',
|
||||
action_params: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'sales team',
|
||||
description: 'This is our internal sales team',
|
||||
allow_auto_assign: true,
|
||||
account_id: 1,
|
||||
is_member: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const finalResult = [
|
||||
{
|
||||
action_name: 'add_label',
|
||||
action_params: ['testlabel'],
|
||||
},
|
||||
{
|
||||
action_name: 'assign_team',
|
||||
action_params: [1],
|
||||
},
|
||||
];
|
||||
|
||||
describe('#actionQueryGenerator', () => {
|
||||
it('returns the correct format of filter query', () => {
|
||||
expect(actionQueryGenerator(testData)).toEqual(finalResult);
|
||||
expect(
|
||||
actionQueryGenerator(testData).every(i => Array.isArray(i.action_params))
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@ const testData = [
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: [
|
||||
{ id: 'pending', name: 'Pending' },
|
||||
{ id: 'PENDING', name: 'Pending' },
|
||||
{ id: 'resolved', name: 'Resolved' },
|
||||
],
|
||||
query_operator: 'and',
|
||||
@@ -31,7 +31,7 @@ const testData = [
|
||||
attribute_key: 'id',
|
||||
filter_operator: 'equal_to',
|
||||
values: 'This is a test',
|
||||
query_operator: null,
|
||||
query_operator: 'or',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -52,8 +52,7 @@ const finalResult = {
|
||||
{
|
||||
attribute_key: 'id',
|
||||
filter_operator: 'equal_to',
|
||||
values: ['This is a test'],
|
||||
query_operator: null,
|
||||
values: ['this is a test'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"FILTER": {
|
||||
"TITLE": "تصفية المحادثة",
|
||||
"TITLE": "تصفية المحادثات",
|
||||
"SUBTITLE": "إضافة فلاتر أدناه واضغط على 'إرسال' لتصفية المحادثات.",
|
||||
"ADD_NEW_FILTER": "إضافة فلتر",
|
||||
"FILTER_DELETE_ERROR": "يجب ان يكون لديك فلتر واحد على الاقل",
|
||||
@@ -19,7 +19,9 @@
|
||||
"contains": "يحتوي",
|
||||
"does_not_contain": "لا يحتوي",
|
||||
"is_present": "موجود",
|
||||
"is_not_present": "غير موجود"
|
||||
"is_not_present": "غير موجود",
|
||||
"is_greater_than": "هو أكبر من",
|
||||
"is_lesser_than": "هو أقل من"
|
||||
},
|
||||
"ATTRIBUTES": {
|
||||
"STATUS": "الحالة",
|
||||
@@ -31,7 +33,54 @@
|
||||
"LABELS": "الوسوم",
|
||||
"BROWSER_LANGUAGE": "لغة المتصفح",
|
||||
"COUNTRY_NAME": "اسم الدولة",
|
||||
"REFERER_LINK": "رابط المرجع"
|
||||
"REFERER_LINK": "رابط المرجع",
|
||||
"CUSTOM_ATTRIBUTE_LIST": "القائمة",
|
||||
"CUSTOM_ATTRIBUTE_TEXT": "النص",
|
||||
"CUSTOM_ATTRIBUTE_NUMBER": "العدد",
|
||||
"CUSTOM_ATTRIBUTE_LINK": "الرابط",
|
||||
"CUSTOM_ATTRIBUTE_CHECKBOX": "مربع"
|
||||
},
|
||||
"GROUPS": {
|
||||
"STANDARD_FILTERS": "الفلاتر القياسية",
|
||||
"ADDITIONAL_FILTERS": "فلاتر إضافية",
|
||||
"CUSTOM_ATTRIBUTES": "سمات مخصصة"
|
||||
},
|
||||
"CUSTOM_VIEWS": {
|
||||
"ADD": {
|
||||
"TITLE": "هل تريد حفظ هذا الفلتر؟",
|
||||
"LABEL": "تسمية هذا الفلتر",
|
||||
"PLACEHOLDER": "أدخل اسم لهذا الفلتر",
|
||||
"ERROR_MESSAGE": "الاسم مطلوب",
|
||||
"SAVE_BUTTON": "حفظ الفلتر",
|
||||
"CANCEL_BUTTON": "إلغاء",
|
||||
"API_FOLDERS": {
|
||||
"SUCCESS_MESSAGE": "تم إنشاء طريقة عرض مخصصة بنجاح",
|
||||
"ERROR_MESSAGE": "خطأ أثناء إنشاء طريقة عرض مخصصة"
|
||||
},
|
||||
"API_SEGMENTS": {
|
||||
"SUCCESS_MESSAGE": "تم إنشاء طريقة عرض مخصصة بنجاح",
|
||||
"ERROR_MESSAGE": "خطأ أثناء إنشاء طريقة عرض مخصصة"
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
"DELETE_BUTTON": "حذف الفلتر",
|
||||
"MODAL": {
|
||||
"CONFIRM": {
|
||||
"TITLE": "تأكيد الحذف",
|
||||
"MESSAGE": "هل أنت متأكد من حذف الفلتر ",
|
||||
"YES": "نعم، احذف",
|
||||
"NO": "لا، احتفظ به"
|
||||
}
|
||||
},
|
||||
"API_FOLDERS": {
|
||||
"SUCCESS_MESSAGE": "تم حذف طريقة عرض مخصصة بنجاح",
|
||||
"ERROR_MESSAGE": "حدث خطأ أثناء حذف المجلد"
|
||||
},
|
||||
"API_SEGMENTS": {
|
||||
"SUCCESS_MESSAGE": "تم حذف العرض المخصص بنجاح",
|
||||
"ERROR_MESSAGE": "حدث خطأ أثناء حذف طريقة عرض مخصصة"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,89 @@
|
||||
{
|
||||
"AUTOMATION": {
|
||||
"HEADER": "الأتمتة",
|
||||
"HEADER_BTN_TXT": "إضافة قاعدة أتمتة"
|
||||
"HEADER_BTN_TXT": "إضافة قاعدة أتمتة",
|
||||
"LOADING": "جلب قواعد الأتمتة",
|
||||
"SIDEBAR_TXT": "<p><b>قواعد الأتمتة الآليه</b> <p>يمكن للأتمتة استبدال وأتمتة العمليات القائمة التي تتطلب جهداً يدوياً. يمكنك القيام بالعديد من الأشياء مع التشغيل الآلي، بما في ذلك إضافة تسميات وتعيين المحادثة لأفضل وكيل. لذا يركز الفريق على ما يفعلونه على أفضل وجه ويقضي وقتاً قليلاً على المهام اليدوية.</p>",
|
||||
"ADD": {
|
||||
"TITLE": "إضافة قاعدة أتمتة",
|
||||
"SUBMIT": "إنشاء",
|
||||
"CANCEL_BUTTON_TEXT": "إلغاء",
|
||||
"FORM": {
|
||||
"NAME": {
|
||||
"LABEL": "اسم القاعدة",
|
||||
"PLACEHOLDER": "أدخل اسم القاعدة",
|
||||
"ERROR": "الاسم مطلوب"
|
||||
},
|
||||
"DESC": {
|
||||
"LABEL": "الوصف",
|
||||
"PLACEHOLDER": "ادخل وصف القاعدة",
|
||||
"ERROR": "الوصف مطلوب"
|
||||
},
|
||||
"EVENT": {
|
||||
"LABEL": "الحدث",
|
||||
"PLACEHOLDER": "الرجاء اختيار واحد",
|
||||
"ERROR": "الحدث مطلوب"
|
||||
},
|
||||
"CONDITIONS": {
|
||||
"LABEL": "الشروط"
|
||||
},
|
||||
"ACTIONS": {
|
||||
"LABEL": "الإجراءات"
|
||||
}
|
||||
},
|
||||
"CONDITION_BUTTON_LABEL": "إضافة شرط",
|
||||
"ACTION_BUTTON_LABEL": "إضافة إجراء",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "تمت إضافة قاعدة الأتمتة بنجاح",
|
||||
"ERROR_MESSAGE": "تعذر إنشاء قاعدة أتمتة ، يرجى المحاولة مرة أخرى لاحقاً"
|
||||
}
|
||||
},
|
||||
"LIST": {
|
||||
"TABLE_HEADER": [
|
||||
"الاسم",
|
||||
"الوصف",
|
||||
"مفعل",
|
||||
"تم إنشاؤها في"
|
||||
],
|
||||
"404": "لم يتم العثور على قواعد أتمتة"
|
||||
},
|
||||
"DELETE": {
|
||||
"TITLE": "حذف قاعدة الأتمتة",
|
||||
"SUBMIT": "حذف",
|
||||
"CANCEL_BUTTON_TEXT": "إلغاء",
|
||||
"CONFIRM": {
|
||||
"TITLE": "تأكيد الحذف",
|
||||
"MESSAGE": "هل أنت متأكد من الحذف ",
|
||||
"YES": "نعم، احذف ",
|
||||
"NO": "لا، احتفظ "
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "تم حذف قاعدة الأتمتة بنجاح",
|
||||
"ERROR_MESSAGE": "تعذر حذف قاعدة الأتمتة، يرجى المحاولة مرة أخرى لاحقاً"
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
"TITLE": "تعديل قاعدة الأتمتة",
|
||||
"SUBMIT": "تعديل",
|
||||
"CANCEL_BUTTON_TEXT": "إلغاء",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "تم تحديث قاعدة الأتمتة بنجاح",
|
||||
"ERROR_MESSAGE": "تعذر تحديث قاعدة الأتمتة، الرجاء المحاولة مرة أخرى في وقت لاحق"
|
||||
}
|
||||
},
|
||||
"CLONE": {
|
||||
"TOOLTIP": "نسخ",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "تم نسخ الأتمتة بنجاح",
|
||||
"ERROR_MESSAGE": "تعذر استنساخ قاعدة الأتمتة، الرجاء المحاولة مرة أخرى لاحقاً"
|
||||
}
|
||||
},
|
||||
"FORM": {
|
||||
"EDIT": "تعديل",
|
||||
"CREATE": "إنشاء",
|
||||
"DELETE": "حذف",
|
||||
"CANCEL": "إلغاء",
|
||||
"RESET_MESSAGE": "تغيير نوع الحدث سوف يعيد تعيين الشروط والأحداث التي أضفتها أدناه"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "إضافة رد جاهز",
|
||||
"DESC": "الردود الجاهزة هي قوالب رسائل معدة مسبقاً يمكن استخدامها لتسريع كتابة الردود في المحادثات .",
|
||||
"DESC": "الردود الجاهزة هي قوالب رسائل معدة مسبقاً يمكن استخدامها لتسريع كتابة الردود في المحادثات.",
|
||||
"CANCEL_BUTTON_TEXT": "إلغاء",
|
||||
"FORM": {
|
||||
"SHORT_CODE": {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"404": "لا توجد محادثات نشطة في هذه المجموعة."
|
||||
},
|
||||
"TAB_HEADING": "المحادثات",
|
||||
"MENTION_HEADING": "Mentions",
|
||||
"MENTION_HEADING": "الإشارات",
|
||||
"SEARCH": {
|
||||
"INPUT": "البحث عن جهات الاتصال، المحادثات، قوالب الردود الجاهزة .."
|
||||
},
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
"LABEL": "رقم الهاتف",
|
||||
"HELP": "يجب ان يحتوى رقم الهاتف على كود دولتك تسبقها علامة +\nمثال: +20101243567",
|
||||
"ERROR": "يجب ان تكون خانة رقم الهاتف إما فارغة او مكتملة مع رمز الدولة",
|
||||
"DUPLICATE": "This phone number is in use for another contact."
|
||||
"DUPLICATE": "رقم الهاتف هذا مستخدم لجهة اتصال أخرى."
|
||||
},
|
||||
"LOCATION": {
|
||||
"PLACEHOLDER": "أدخل موقع جهة الاتصال",
|
||||
@@ -169,6 +169,7 @@
|
||||
"SUBMIT": "إرسال الرسالة",
|
||||
"CANCEL": "إلغاء",
|
||||
"SUCCESS_MESSAGE": "تم إرسال الرسالة!",
|
||||
"GO_TO_CONVERSATION": "عرض",
|
||||
"ERROR_MESSAGE": "تعذر الإرسال! حاول مرة أخرى"
|
||||
}
|
||||
},
|
||||
@@ -177,7 +178,9 @@
|
||||
"FIELDS": "تصنفيات جهات الاتصال",
|
||||
"SEARCH_BUTTON": "بحث",
|
||||
"SEARCH_INPUT_PLACEHOLDER": "بحث عن جهات الاتصال",
|
||||
"FILTER_CONTACTS": "Filter",
|
||||
"FILTER_CONTACTS": "فلترة",
|
||||
"FILTER_CONTACTS_SAVE": "حفظ الفلتر",
|
||||
"FILTER_CONTACTS_DELETE": "حذف الفلتر",
|
||||
"LIST": {
|
||||
"LOADING_MESSAGE": "جاري تحميل جهات الاتصال...",
|
||||
"404": "لا توجد جهات اتصال تطابق بحثك 🔍",
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"CONTACTS_FILTER": {
|
||||
"TITLE": "Filter Contacts",
|
||||
"SUBTITLE": "Add filters below and hit 'Submit' to filter contacts.",
|
||||
"TITLE": "تصفية جهات الاتصال",
|
||||
"SUBTITLE": "إضافة فلاتر أدناه واضغط على 'إرسال' لتصفية جهات الاتصال.",
|
||||
"ADD_NEW_FILTER": "إضافة فلتر",
|
||||
"CLEAR_ALL_FILTERS": "Clear All Filters",
|
||||
"CLEAR_ALL_FILTERS": "مسح جميع الفلاتر",
|
||||
"FILTER_DELETE_ERROR": "يجب ان يكون لديك فلتر واحد على الاقل",
|
||||
"SUBMIT_BUTTON_LABEL": "إرسال",
|
||||
"CANCEL_BUTTON_LABEL": "إلغاء",
|
||||
"CLEAR_BUTTON_LABEL": "مسح الفلاتر",
|
||||
"EMPTY_VALUE_ERROR": "القيمة مطلوبة",
|
||||
"TOOLTIP_LABEL": "Filter contacts",
|
||||
"TOOLTIP_LABEL": "تصفية جهات الاتصال",
|
||||
"QUERY_DROPDOWN_LABELS": {
|
||||
"AND": "و",
|
||||
"OR": "أو"
|
||||
@@ -20,15 +20,27 @@
|
||||
"contains": "يحتوي",
|
||||
"does_not_contain": "لا يحتوي",
|
||||
"is_present": "موجود",
|
||||
"is_not_present": "غير موجود"
|
||||
"is_not_present": "غير موجود",
|
||||
"is_greater_than": "هو أكبر من",
|
||||
"is_lesser_than": "هو أقل من"
|
||||
},
|
||||
"ATTRIBUTES": {
|
||||
"NAME": "الاسم",
|
||||
"EMAIL": "البريد الإلكتروني",
|
||||
"PHONE_NUMBER": "رقم الهاتف",
|
||||
"IDENTIFIER": "Identifier",
|
||||
"IDENTIFIER": "المعرف",
|
||||
"CITY": "المدينة",
|
||||
"COUNTRY": "الدولة"
|
||||
"COUNTRY": "الدولة",
|
||||
"CUSTOM_ATTRIBUTE_LIST": "القائمة",
|
||||
"CUSTOM_ATTRIBUTE_TEXT": "النص",
|
||||
"CUSTOM_ATTRIBUTE_NUMBER": "العدد",
|
||||
"CUSTOM_ATTRIBUTE_LINK": "الرابط",
|
||||
"CUSTOM_ATTRIBUTE_CHECKBOX": "مربع"
|
||||
},
|
||||
"GROUPS": {
|
||||
"STANDARD_FILTERS": "الفلاتر القياسية",
|
||||
"ADDITIONAL_FILTERS": "فلاتر إضافية",
|
||||
"CUSTOM_ATTRIBUTES": "سمات مخصصة"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
"LOADING_CONVERSATIONS": "جاري تحميل المحادثات",
|
||||
"CANNOT_REPLY": "لا يمكنك الرد بسبب",
|
||||
"24_HOURS_WINDOW": "قيد نافذة الـ 24 ساعة",
|
||||
"NOT_ASSIGNED_TO_YOU": "لم يتم تعيين هذه المحادثة لك. هل ترغب في تعيين هذه المحادثة لنفسك؟",
|
||||
"ASSIGN_TO_ME": "إسناد لي",
|
||||
"TWILIO_WHATSAPP_CAN_REPLY": "يمكنك فقط الرد على هذه المحادثة باستخدام رسالة قالب بسبب",
|
||||
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "قيد نافذة الـ 24 ساعة",
|
||||
"SELECT_A_TWEET_TO_REPLY": "الرجاء تحديد تغريدة للرد عليها.",
|
||||
@@ -55,6 +57,9 @@
|
||||
}
|
||||
},
|
||||
"FOOTER": {
|
||||
"MESSAGE_SIGN_TOOLTIP": "Message signature",
|
||||
"ENABLE_SIGN_TOOLTIP": "Enable signature",
|
||||
"DISABLE_SIGN_TOOLTIP": "Disable signature",
|
||||
"MSG_INPUT": "زر Shift + Enter لإضافة سطر جديد. ابدأ بزر / للاختيار من الردود السريعة.",
|
||||
"PRIVATE_MSG_INPUT": "زر Shift + Enter لإضافة سطر جديد. سيكون هذا مرئياً للموظفين فقط"
|
||||
},
|
||||
@@ -90,6 +95,9 @@
|
||||
"FILE_SIZE_LIMIT": "حجم الملف يتجاوز حد الاقصى وهو {MAXIMUM_FILE_UPLOAD_SIZE}",
|
||||
"MESSAGE_ERROR": "غير قادر على إرسال هذه الرسالة، الرجاء المحاولة مرة أخرى لاحقاً",
|
||||
"SENT_BY": "أرسلت بواسطة:",
|
||||
"BOT": "رد آلي",
|
||||
"SEND_FAILED": "تعذر إرسال الرسالة! حاول مرة أخرى",
|
||||
"TRY_AGAIN": "إعادة المحاولة",
|
||||
"ASSIGNMENT": {
|
||||
"SELECT_AGENT": "اختر وكيل",
|
||||
"REMOVE": "حذف",
|
||||
@@ -179,6 +187,7 @@
|
||||
}
|
||||
},
|
||||
"EMAIL_HEADER": {
|
||||
"FROM": "من",
|
||||
"TO": "إلى",
|
||||
"BCC": "Bcc",
|
||||
"CC": "Cc",
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
"NOTE": ""
|
||||
},
|
||||
"ACCOUNT_ID": {
|
||||
"TITLE": "Account ID",
|
||||
"NOTE": "This ID is required if you are building an API based integration"
|
||||
"TITLE": "معرف الحساب",
|
||||
"NOTE": "هذا المعرف مطلوب إذا كنت بصدد بناء تكامل على API"
|
||||
},
|
||||
"NAME": {
|
||||
"LABEL": "اسم الحساب",
|
||||
@@ -40,7 +40,7 @@
|
||||
"AUTO_RESOLVE_DURATION": {
|
||||
"LABEL": "عدد الأيام بعد التذكرة التي يجب أن يحل تلقائياً إذا لم يكن هناك أي نشاط",
|
||||
"PLACEHOLDER": "30",
|
||||
"ERROR": "الرجاء إدخال مدة الحل التلقائي صحيحة (يوم واحد على الأقل)"
|
||||
"ERROR": "الرجاء إدخال مدة حل تلقائي صالحة (حد أدنى 1 يوم والحد الأقصى 999 يوما)"
|
||||
},
|
||||
"FEATURES": {
|
||||
"INBOUND_EMAIL_ENABLED": "الاستمرار في المحادثة عبر رسائل البريد الإلكتروني مفعّل لحسابك.",
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
"HELP": "لإضافة حساب تويتر الخاص بك كقناة تواصل، تحتاج إلى مصادقة حسابك على تويتر بك بالنقر على زر \"تسجيل الدخول باستخدام تويتر\" ",
|
||||
"ERROR_MESSAGE": "حدث خطأ أثناء الاتصال بـ Twitter، الرجاء المحاولة مرة أخرى",
|
||||
"TWEETS": {
|
||||
"ENABLE": "Create conversations from mentioned Tweets"
|
||||
"ENABLE": "إنشاء محادثات من التغريدات المشار إليها"
|
||||
}
|
||||
},
|
||||
"WEBSITE_CHANNEL": {
|
||||
@@ -136,8 +136,56 @@
|
||||
}
|
||||
},
|
||||
"SMS": {
|
||||
"TITLE": "قناة SMS عبر Twilio",
|
||||
"DESC": "ابدأ في دعم عملائك عبر الرسائل القصيرة بإستخدام Twilio."
|
||||
"TITLE": "قناة SMS",
|
||||
"DESC": "ابدأ في دعم عملائك عبر الرسائل القصيرة.",
|
||||
"PROVIDERS": {
|
||||
"LABEL": "API Provider",
|
||||
"TWILIO": "تويليو",
|
||||
"BANDWIDTH": "سعة الإنترنت"
|
||||
},
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "لم نتمكن من حفظ قناة الرسائل القصيرة"
|
||||
},
|
||||
"BANDWIDTH": {
|
||||
"ACCOUNT_ID": {
|
||||
"LABEL": "معرف الحساب",
|
||||
"PLACEHOLDER": "الرجاء إدخال معرف حساب النطاق الترددي الخاص بك",
|
||||
"ERROR": "هذا الحقل مطلوب"
|
||||
},
|
||||
"API_KEY": {
|
||||
"LABEL": "مفتاح API",
|
||||
"PLACEHOLDER": "الرجاء إدخال مفتاح API الخاص بك",
|
||||
"ERROR": "هذا الحقل مطلوب"
|
||||
},
|
||||
"API_SECRET": {
|
||||
"LABEL": "سرية API",
|
||||
"PLACEHOLDER": "الرجاء إدخال مفتاح API الخاص بك",
|
||||
"ERROR": "هذا الحقل مطلوب"
|
||||
},
|
||||
"APPLICATION_ID": {
|
||||
"LABEL": "معرف التطبيق",
|
||||
"PLACEHOLDER": "الرجاء إدخال معرف تطبيق النطاق الترددي الخاص بك",
|
||||
"ERROR": "هذا الحقل مطلوب"
|
||||
},
|
||||
"INBOX_NAME": {
|
||||
"LABEL": "اسم صندوق الوارد لقناة التواصل",
|
||||
"PLACEHOLDER": "الرجاء إدخال اسم القناة",
|
||||
"ERROR": "هذا الحقل مطلوب"
|
||||
},
|
||||
"PHONE_NUMBER": {
|
||||
"LABEL": "رقم الهاتف",
|
||||
"PLACEHOLDER": "الرجاء إدخال رقم الهاتف الذي سيتم إرسال الرسائل منه.",
|
||||
"ERROR": "الرجاء إدخال قيمة صحيحة. يجب أن يبدأ رقم الهاتف بعلامة `+`."
|
||||
},
|
||||
"SUBMIT_BUTTON": "إنشاء قناة عرض التردد",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "تعذر تكوين المصادقة بواسطة بيانات الاعتماد الخاصة بحسابك على Twilio، يرجى المحاولة مرة أخرى"
|
||||
},
|
||||
"API_CALLBACK": {
|
||||
"TITLE": "عنوان Callback URL",
|
||||
"SUBTITLE": "يتوجب تهيئة عنوان callback URL في إعدادات Twilio بإدخال القيمة أدناه."
|
||||
}
|
||||
}
|
||||
},
|
||||
"WHATSAPP": {
|
||||
"TITLE": "قناة واتساب",
|
||||
@@ -305,6 +353,14 @@
|
||||
"ENABLED": "مفعل",
|
||||
"DISABLED": "معطّل"
|
||||
},
|
||||
"ALLOW_MESSAGES_AFTER_RESOLVED": {
|
||||
"ENABLED": "مفعل",
|
||||
"DISABLED": "معطّل"
|
||||
},
|
||||
"ENABLE_CONTINUITY_VIA_EMAIL": {
|
||||
"ENABLED": "مفعل",
|
||||
"DISABLED": "معطّل"
|
||||
},
|
||||
"ENABLE_HMAC": {
|
||||
"LABEL": "تمكين"
|
||||
}
|
||||
@@ -351,6 +407,8 @@
|
||||
"AUTO_ASSIGNMENT": "تفعيل الإسناد التلقائي",
|
||||
"ENABLE_CSAT": "تمكين تقييم خدمة العملاء",
|
||||
"ENABLE_CSAT_SUB_TEXT": "تمكين/تعطيل تقييم خدمة العملاء بعد إنتهاء المحادثة",
|
||||
"ENABLE_CONTINUITY_VIA_EMAIL": "تمكين استمرارية المحادثة عبر البريد الإلكتروني",
|
||||
"ENABLE_CONTINUITY_VIA_EMAIL_SUB_TEXT": "المحادثات ستستمر عبر البريد الإلكتروني إذا كان عنوان البريد الإلكتروني لجهة الاتصال متاحاً.",
|
||||
"INBOX_UPDATE_TITLE": "إعدادات قناة التواصل",
|
||||
"INBOX_UPDATE_SUB_TEXT": "تحديث إعدادات قناة التواصل",
|
||||
"AUTO_ASSIGNMENT_SUB_TEXT": "تمكين أو تعطيل الإسناد التلقائي للمحادثات الجديدة إلى الموظفين المضافين إلى قناة التواصل هذه.",
|
||||
@@ -361,7 +419,9 @@
|
||||
"INBOX_IDENTIFIER": "معرف صندوق الوارد",
|
||||
"INBOX_IDENTIFIER_SUB_TEXT": "استخدم رمز 'inbox_identifier' المعروض هنا للمصادقة على عملاء API الخاص بك.",
|
||||
"FORWARD_EMAIL_TITLE": "إعادة التوجيه إلى البريد الإلكتروني",
|
||||
"FORWARD_EMAIL_SUB_TEXT": "بدء إعادة توجيه رسائل البريد الإلكتروني الخاصة بك إلى عنوان البريد الإلكتروني التالي."
|
||||
"FORWARD_EMAIL_SUB_TEXT": "بدء إعادة توجيه رسائل البريد الإلكتروني الخاصة بك إلى عنوان البريد الإلكتروني التالي.",
|
||||
"ALLOW_MESSAGES_AFTER_RESOLVED": "السماح بالرسائل بعد حل المحادثة",
|
||||
"ALLOW_MESSAGES_AFTER_RESOLVED_SUB_TEXT": "السماح للمستخدمين النهائيين بإرسال رسائل حتى بعد تسوية المحادثة."
|
||||
},
|
||||
"FACEBOOK_REAUTHORIZE": {
|
||||
"TITLE": "إعادة التصريح",
|
||||
@@ -461,7 +521,11 @@
|
||||
"DOMAIN": {
|
||||
"LABEL": "الدومين",
|
||||
"PLACE_HOLDER": "الدومين"
|
||||
}
|
||||
},
|
||||
"ENCRYPTION": "التشفير",
|
||||
"SSL_TLS": "SSL/TLS",
|
||||
"START_TLS": "STARTTLS",
|
||||
"OPEN_SSL_VERIFY_MODE": "فتح وضع التحقق من SSL"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { default as _advancedFilters } from './advancedFilters.json';
|
||||
import { default as _agentMgmt } from './agentMgmt.json';
|
||||
import { default as _attributesMgmt } from './attributesMgmt.json';
|
||||
import { default as _automation } from './automation.json';
|
||||
import { default as _campaign } from './campaign.json';
|
||||
import { default as _cannedMgmt } from './cannedMgmt.json';
|
||||
import { default as _chatlist } from './chatlist.json';
|
||||
import { default as _contact } from './contact.json';
|
||||
import { default as _contactFilters } from './contactFilters.json';
|
||||
import { default as _conversation } from './conversation.json';
|
||||
import { default as _csatMgmtMgmt } from './csatMgmt.json';
|
||||
import { default as _generalSettings } from './generalSettings.json';
|
||||
@@ -20,12 +23,15 @@ import { default as _signup } from './signup.json';
|
||||
import { default as _teamsSettings } from './teamsSettings.json';
|
||||
|
||||
export default {
|
||||
..._advancedFilters,
|
||||
..._agentMgmt,
|
||||
..._attributesMgmt,
|
||||
..._automation,
|
||||
..._campaign,
|
||||
..._cannedMgmt,
|
||||
..._chatlist,
|
||||
..._contact,
|
||||
..._contactFilters,
|
||||
..._conversation,
|
||||
..._csatMgmtMgmt,
|
||||
..._generalSettings,
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
"TITLE": "إعدادات الملف الشخصي",
|
||||
"BTN_TEXT": "تعديل الملف الشخصي",
|
||||
"DELETE_AVATAR": "حذف الصورة الرمزية",
|
||||
"AVATAR_DELETE_SUCCESS": "Avatar has been deleted successfully",
|
||||
"AVATAR_DELETE_FAILED": "There is an error while deleting avatar, please try again",
|
||||
"AVATAR_DELETE_SUCCESS": "تم حذف الصورة الرمزية بنجاح",
|
||||
"AVATAR_DELETE_FAILED": "حدث خطأ أثناء حذف الصورة الرمزية، الرجاء المحاولة مرة أخرى",
|
||||
"UPDATE_SUCCESS": "تم تحديث حسابك بنجاح",
|
||||
"PASSWORD_UPDATE_SUCCESS": "تم تغيير كلمة المرور بنجاح",
|
||||
"AFTER_EMAIL_CHANGED": "تم تحديث ملفك الشخصي بنجاح، الرجاء تسجيل الدخول مرة أخرى حيث أنه قد تم تغيير بيانات تسجيل الدخول الخاصة بك",
|
||||
@@ -19,6 +19,18 @@
|
||||
"TITLE": "الملف الشخصي",
|
||||
"NOTE": "عنوان بريدك الإلكتروني هو المعرف الخاص بك الذي ستستخدمه لتسجيل الدخول."
|
||||
},
|
||||
"MESSAGE_SIGNATURE_SECTION": {
|
||||
"TITLE": "Personal message signature",
|
||||
"NOTE": "Create a personal message signature that would be added to all the messages you send from the platform. Use the rich content editor to create a highly personalised signature.",
|
||||
"BTN_TEXT": "Save message signature",
|
||||
"API_ERROR": "Couldn't save signature! Try again",
|
||||
"API_SUCCESS": "Signature saved successfully"
|
||||
},
|
||||
"MESSAGE_SIGNATURE": {
|
||||
"LABEL": "Message Signature",
|
||||
"ERROR": "Message Signature cannot be empty",
|
||||
"PLACEHOLDER": "Insert your personal message signature here."
|
||||
},
|
||||
"PASSWORD_SECTION": {
|
||||
"TITLE": "كلمة المرور",
|
||||
"NOTE": "تعديل كلمة المرور الخاصة بك سيعيد ضبط تسجيلات الدخول الخاصة بك في الأجهزة الأخرى.",
|
||||
@@ -89,7 +101,7 @@
|
||||
"PLACEHOLDER": "الرجاء إدخال كلمة مرور جديدة"
|
||||
},
|
||||
"PASSWORD": {
|
||||
"LABEL": "كلمة المرور",
|
||||
"LABEL": "كلمة مرور جديدة",
|
||||
"ERROR": "الرجاء إدخال كلمة مرور بطول 6 أحرف أو أكثر",
|
||||
"PLACEHOLDER": "الرجاء إدخال كلمة مرور جديدة"
|
||||
},
|
||||
@@ -103,7 +115,7 @@
|
||||
"SIDEBAR_ITEMS": {
|
||||
"CHANGE_AVAILABILITY_STATUS": "يتغيرون",
|
||||
"CHANGE_ACCOUNTS": "تبديل الحساب",
|
||||
"CONTACT_SUPPORT": "Contact Support",
|
||||
"CONTACT_SUPPORT": "تواصل مع الدعم",
|
||||
"SELECTOR_SUBTITLE": "اختر حساباً من القائمة التالية",
|
||||
"PROFILE_SETTINGS": "إعدادات الملف الشخصي",
|
||||
"KEYBOARD_SHORTCUTS": "اختصارات لوحة المفاتيح",
|
||||
@@ -136,7 +148,7 @@
|
||||
"SIDEBAR": {
|
||||
"CONVERSATIONS": "المحادثات",
|
||||
"ALL_CONVERSATIONS": "كل المحادثات",
|
||||
"MENTIONED_CONVERSATIONS": "Mentions",
|
||||
"MENTIONED_CONVERSATIONS": "الإشارات",
|
||||
"REPORTS": "التقارير",
|
||||
"SETTINGS": "الإعدادات",
|
||||
"CONTACTS": "جهات الاتصال",
|
||||
@@ -153,11 +165,13 @@
|
||||
"CUSTOM_ATTRIBUTES": "سمات مخصصة",
|
||||
"AUTOMATION": "الأتمتة",
|
||||
"TEAMS": "الفرق",
|
||||
"CUSTOM_VIEWS_FOLDER": "المجلدات",
|
||||
"CUSTOM_VIEWS_SEGMENTS": "الأجزاء",
|
||||
"ALL_CONTACTS": "جميع جهات الاتصال",
|
||||
"TAGGED_WITH": "مشار إليه بواسطة",
|
||||
"NEW_LABEL": "New label",
|
||||
"NEW_TEAM": "New team",
|
||||
"NEW_INBOX": "New inbox",
|
||||
"NEW_LABEL": "علامة جديدة",
|
||||
"NEW_TEAM": "فريق جديد",
|
||||
"NEW_INBOX": "صندوق الوارد الجديد",
|
||||
"REPORTS_OVERVIEW": "نظرة عامة",
|
||||
"CSAT": "CSAT",
|
||||
"CAMPAIGNS": "الحملات",
|
||||
@@ -167,7 +181,7 @@
|
||||
"REPORTS_LABEL": "الوسوم",
|
||||
"REPORTS_INBOX": "صندوق الوارد",
|
||||
"REPORTS_TEAM": "الفريق",
|
||||
"SET_AVAILABILITY_TITLE": "Set yourself as"
|
||||
"SET_AVAILABILITY_TITLE": "تعيين نفسك كـ"
|
||||
},
|
||||
"CREATE_ACCOUNT": {
|
||||
"NO_ACCOUNT_WARNING": "أوه! لم نتمكن من العثور على الحساب. الرجاء إنشاء حساب جديد للمتابعة.",
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
"contains": "Съдържа",
|
||||
"does_not_contain": "Не съдържа",
|
||||
"is_present": "Присъства",
|
||||
"is_not_present": "Не присъства"
|
||||
"is_not_present": "Не присъства",
|
||||
"is_greater_than": "Is greater than",
|
||||
"is_lesser_than": "Is lesser than"
|
||||
},
|
||||
"ATTRIBUTES": {
|
||||
"STATUS": "Статус",
|
||||
@@ -31,7 +33,54 @@
|
||||
"LABELS": "Етикети",
|
||||
"BROWSER_LANGUAGE": "Език на браузъра",
|
||||
"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"
|
||||
},
|
||||
"GROUPS": {
|
||||
"STANDARD_FILTERS": "Standard Filters",
|
||||
"ADDITIONAL_FILTERS": "Additional Filters",
|
||||
"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",
|
||||
"SAVE_BUTTON": "Save filter",
|
||||
"CANCEL_BUTTON": "Отмени",
|
||||
"API_FOLDERS": {
|
||||
"SUCCESS_MESSAGE": "Folder created successfully",
|
||||
"ERROR_MESSAGE": "Error while creating folder"
|
||||
},
|
||||
"API_SEGMENTS": {
|
||||
"SUCCESS_MESSAGE": "Segment created successfully",
|
||||
"ERROR_MESSAGE": "Error while creating segment"
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
"DELETE_BUTTON": "Delete filter",
|
||||
"MODAL": {
|
||||
"CONFIRM": {
|
||||
"TITLE": "Потвърди изтриването",
|
||||
"MESSAGE": "Are you sure to delete the filter ",
|
||||
"YES": "Да, изтрий",
|
||||
"NO": "No, Keep it"
|
||||
}
|
||||
},
|
||||
"API_FOLDERS": {
|
||||
"SUCCESS_MESSAGE": "Folder deleted successfully",
|
||||
"ERROR_MESSAGE": "Error while deleting folder"
|
||||
},
|
||||
"API_SEGMENTS": {
|
||||
"SUCCESS_MESSAGE": "Segment deleted successfully",
|
||||
"ERROR_MESSAGE": "Error while deleting segment"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,89 @@
|
||||
{
|
||||
"AUTOMATION": {
|
||||
"HEADER": "Автоматизация",
|
||||
"HEADER_BTN_TXT": "Добавяне правило за автоматизация"
|
||||
"HEADER_BTN_TXT": "Добавяне правило за автоматизация",
|
||||
"LOADING": "Fetching automation rules",
|
||||
"SIDEBAR_TXT": "<p><b>Automation Rules</b> <p>Automation can replace and automate existing processes that require manual effort. You can do many things with automation, including adding labels and assigning conversation to the best agent. So the team focuses on what they do best and spends more little time on manual tasks.</p>",
|
||||
"ADD": {
|
||||
"TITLE": "Добавяне правило за автоматизация",
|
||||
"SUBMIT": "Създаване",
|
||||
"CANCEL_BUTTON_TEXT": "Отмени",
|
||||
"FORM": {
|
||||
"NAME": {
|
||||
"LABEL": "Rule Name",
|
||||
"PLACEHOLDER": "Enter rule name",
|
||||
"ERROR": "Name is required"
|
||||
},
|
||||
"DESC": {
|
||||
"LABEL": "Описание",
|
||||
"PLACEHOLDER": "Enter rule description",
|
||||
"ERROR": "Description is required"
|
||||
},
|
||||
"EVENT": {
|
||||
"LABEL": "Event",
|
||||
"PLACEHOLDER": "Please select one",
|
||||
"ERROR": "Event is required"
|
||||
},
|
||||
"CONDITIONS": {
|
||||
"LABEL": "Conditions"
|
||||
},
|
||||
"ACTIONS": {
|
||||
"LABEL": "Действия"
|
||||
}
|
||||
},
|
||||
"CONDITION_BUTTON_LABEL": "Add Condition",
|
||||
"ACTION_BUTTON_LABEL": "Add Action",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Automation rule added successfully",
|
||||
"ERROR_MESSAGE": "Could not able to create a automation rule, Please try again later"
|
||||
}
|
||||
},
|
||||
"LIST": {
|
||||
"TABLE_HEADER": [
|
||||
"Име",
|
||||
"Описание",
|
||||
"Активен",
|
||||
"Created on"
|
||||
],
|
||||
"404": "No automation rules found"
|
||||
},
|
||||
"DELETE": {
|
||||
"TITLE": "Delete Automation Rule",
|
||||
"SUBMIT": "Изтрий",
|
||||
"CANCEL_BUTTON_TEXT": "Отмени",
|
||||
"CONFIRM": {
|
||||
"TITLE": "Потвърди изтриването",
|
||||
"MESSAGE": "Сигурни ли сте за изтриването ",
|
||||
"YES": "Да, изтрий ",
|
||||
"NO": "Не, запази "
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Automation rule deleted successfully",
|
||||
"ERROR_MESSAGE": "Could not able to delete a automation rule, Please try again later"
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
"TITLE": "Edit Automation Rule",
|
||||
"SUBMIT": "Редактирай",
|
||||
"CANCEL_BUTTON_TEXT": "Отмени",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Automation rule updated successfully",
|
||||
"ERROR_MESSAGE": "Could not update automation rule, Please try again later"
|
||||
}
|
||||
},
|
||||
"CLONE": {
|
||||
"TOOLTIP": "Clone",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Automation cloned successfully",
|
||||
"ERROR_MESSAGE": "Could not clone automation rule, Please try again later"
|
||||
}
|
||||
},
|
||||
"FORM": {
|
||||
"EDIT": "Редактирай",
|
||||
"CREATE": "Създаване",
|
||||
"DELETE": "Изтрий",
|
||||
"CANCEL": "Отмени",
|
||||
"RESET_MESSAGE": "Changing event type will reset the conditions and events you have added below"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"HEADER_BTN_TXT": "Добавяне на готов отговор",
|
||||
"LOADING": "Извличане на готови отговори",
|
||||
"SEARCH_404": "Няма резултати отговарящи на тази заявка",
|
||||
"SIDEBAR_TXT": "<p><b>Готови отговори</b> </p><p> Готовите отговори са запазени шаблони за отговори, които могат да се използват за бързо изпращане на отговор в разговора. </p><p> За да създадете готов отговор, просто щракнете върху <b>Добавяне на готов отговор</b>. Можете също да редактирате или изтриете съществуващ готов отговор, като щракнете върху бутона Редактиране или Изтриване </p><p> Готовите отговори се използват с помощта на <b>Кратки кодове</b>. Агентите имат достъп до готовите отговори, докато са в чат, като напишат <b>'/'</b>, последвано от краткия код. </p>",
|
||||
"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>",
|
||||
"LIST": {
|
||||
"404": "Няма налични готови отговори в този акаунт.",
|
||||
"TITLE": "Управлявайте готовите отговори",
|
||||
@@ -17,12 +17,12 @@
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Добавяне на готов отговор",
|
||||
"DESC": "Готовите отговори са предварително дефинирани шаблони за отговор, които могат да се изпращат бързо в чата.",
|
||||
"DESC": "Canned Responses are saved reply templates which can be used to quickly send out reply to conversation.",
|
||||
"CANCEL_BUTTON_TEXT": "Отмени",
|
||||
"FORM": {
|
||||
"SHORT_CODE": {
|
||||
"LABEL": "Кратък код",
|
||||
"PLACEHOLDER": "Моля, въведете кратък код",
|
||||
"PLACEHOLDER": "Please enter a short code",
|
||||
"ERROR": "Краткия код е задължителен"
|
||||
},
|
||||
"CONTENT": {
|
||||
|
||||
@@ -169,6 +169,7 @@
|
||||
"SUBMIT": "Изпрати съобщение",
|
||||
"CANCEL": "Отмени",
|
||||
"SUCCESS_MESSAGE": "Съобщението е изпратено!",
|
||||
"GO_TO_CONVERSATION": "View",
|
||||
"ERROR_MESSAGE": "Не може да се изпрати! Опитайте пак"
|
||||
}
|
||||
},
|
||||
@@ -178,6 +179,8 @@
|
||||
"SEARCH_BUTTON": "Търсене",
|
||||
"SEARCH_INPUT_PLACEHOLDER": "Търсене на контакти",
|
||||
"FILTER_CONTACTS": "Филтър",
|
||||
"FILTER_CONTACTS_SAVE": "Save filter",
|
||||
"FILTER_CONTACTS_DELETE": "Delete filter",
|
||||
"LIST": {
|
||||
"LOADING_MESSAGE": "Зареждане на контактите...",
|
||||
"404": "Няма контакти отговарящи на търсенети ви 🔍",
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
"contains": "Съдържа",
|
||||
"does_not_contain": "Не съдържа",
|
||||
"is_present": "Присъства",
|
||||
"is_not_present": "Не присъства"
|
||||
"is_not_present": "Не присъства",
|
||||
"is_greater_than": "Is greater than",
|
||||
"is_lesser_than": "Is lesser than"
|
||||
},
|
||||
"ATTRIBUTES": {
|
||||
"NAME": "Име",
|
||||
@@ -28,7 +30,17 @@
|
||||
"PHONE_NUMBER": "Телефон",
|
||||
"IDENTIFIER": "Идентификатор",
|
||||
"CITY": "Град",
|
||||
"COUNTRY": "Държава"
|
||||
"COUNTRY": "Държава",
|
||||
"CUSTOM_ATTRIBUTE_LIST": "List",
|
||||
"CUSTOM_ATTRIBUTE_TEXT": "Text",
|
||||
"CUSTOM_ATTRIBUTE_NUMBER": "Number",
|
||||
"CUSTOM_ATTRIBUTE_LINK": "Link",
|
||||
"CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox"
|
||||
},
|
||||
"GROUPS": {
|
||||
"STANDARD_FILTERS": "Standard Filters",
|
||||
"ADDITIONAL_FILTERS": "Additional Filters",
|
||||
"CUSTOM_ATTRIBUTES": "Персонализирани атрибути"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
"LOADING_CONVERSATIONS": "Loading Conversations",
|
||||
"CANNOT_REPLY": "You cannot reply due to",
|
||||
"24_HOURS_WINDOW": "24 hour message window restriction",
|
||||
"NOT_ASSIGNED_TO_YOU": "This conversation is not assigned to you. Would you like to assign this conversation to yourself?",
|
||||
"ASSIGN_TO_ME": "Assign to me",
|
||||
"TWILIO_WHATSAPP_CAN_REPLY": "You can only reply to this conversation using a template message due to",
|
||||
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "24 hour message window restriction",
|
||||
"SELECT_A_TWEET_TO_REPLY": "Please select a tweet to reply to.",
|
||||
@@ -55,6 +57,9 @@
|
||||
}
|
||||
},
|
||||
"FOOTER": {
|
||||
"MESSAGE_SIGN_TOOLTIP": "Message signature",
|
||||
"ENABLE_SIGN_TOOLTIP": "Enable signature",
|
||||
"DISABLE_SIGN_TOOLTIP": "Disable signature",
|
||||
"MSG_INPUT": "Shift + enter for new line. Start with '/' to select a Canned Response.",
|
||||
"PRIVATE_MSG_INPUT": "Shift + enter for new line. This will be visible only to Agents"
|
||||
},
|
||||
@@ -90,6 +95,9 @@
|
||||
"FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_FILE_UPLOAD_SIZE} attachment limit",
|
||||
"MESSAGE_ERROR": "Unable to send this message, please try again later",
|
||||
"SENT_BY": "Sent by:",
|
||||
"BOT": "Бот",
|
||||
"SEND_FAILED": "Couldn't send message! Try again",
|
||||
"TRY_AGAIN": "retry",
|
||||
"ASSIGNMENT": {
|
||||
"SELECT_AGENT": "Select Agent",
|
||||
"REMOVE": "Remove",
|
||||
@@ -127,7 +135,7 @@
|
||||
},
|
||||
"TEAM_MEMBERS": {
|
||||
"TITLE": "Invite your team members",
|
||||
"DESCRIPTION": "Since you are getting ready to talk to your customer, bring in your teammates to assist you. You can invite your teammates by adding their email address to the agent list.",
|
||||
"DESCRIPTION": "Since you are getting ready to talk to your customer, bring in your teammates to assist you. You can invite your teammates by adding their email addresses to the agent list.",
|
||||
"NEW_LINK": "Click here to invite a team member"
|
||||
},
|
||||
"INBOXES": {
|
||||
@@ -179,6 +187,7 @@
|
||||
}
|
||||
},
|
||||
"EMAIL_HEADER": {
|
||||
"FROM": "From",
|
||||
"TO": "До",
|
||||
"BCC": "Bcc",
|
||||
"CC": "Cc",
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"AUTO_RESOLVE_DURATION": {
|
||||
"LABEL": "Number of days after a ticket should auto resolve if there is no activity",
|
||||
"PLACEHOLDER": "30",
|
||||
"ERROR": "Please enter a valid auto resolve duration (minimum 1 day)"
|
||||
"ERROR": "Please enter a valid auto resolve duration (minimum 1 day and maximum 999 days)"
|
||||
},
|
||||
"FEATURES": {
|
||||
"INBOUND_EMAIL_ENABLED": "Conversation continuity with emails is enabled for your account.",
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
},
|
||||
"CHANNEL_GREETING_TOGGLE": {
|
||||
"LABEL": "Enable channel greeting",
|
||||
"HELP_TEXT": "Send a greeting message to the user when he starts the conversation.",
|
||||
"HELP_TEXT": "Send a greeting message to the users when they starts the conversation.",
|
||||
"ENABLED": "Enabled",
|
||||
"DISABLED": "Disabled"
|
||||
},
|
||||
@@ -136,8 +136,56 @@
|
||||
}
|
||||
},
|
||||
"SMS": {
|
||||
"TITLE": "SMS Channel via Twilio",
|
||||
"DESC": "Start supporting your customers via SMS with Twilio integration."
|
||||
"TITLE": "SMS Channel",
|
||||
"DESC": "Start supporting your customers via SMS.",
|
||||
"PROVIDERS": {
|
||||
"LABEL": "API Provider",
|
||||
"TWILIO": "Twilio",
|
||||
"BANDWIDTH": "Bandwidth"
|
||||
},
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "We were not able to save the SMS channel"
|
||||
},
|
||||
"BANDWIDTH": {
|
||||
"ACCOUNT_ID": {
|
||||
"LABEL": "Account ID",
|
||||
"PLACEHOLDER": "Please enter your Bandwidth Account ID",
|
||||
"ERROR": "This field is required"
|
||||
},
|
||||
"API_KEY": {
|
||||
"LABEL": "API Key",
|
||||
"PLACEHOLDER": "Please enter your Bandwith API Key",
|
||||
"ERROR": "This field is required"
|
||||
},
|
||||
"API_SECRET": {
|
||||
"LABEL": "API Secret",
|
||||
"PLACEHOLDER": "Please enter your Bandwith API Secret",
|
||||
"ERROR": "This field is required"
|
||||
},
|
||||
"APPLICATION_ID": {
|
||||
"LABEL": "Application ID",
|
||||
"PLACEHOLDER": "Please enter your Bandwidth Application ID",
|
||||
"ERROR": "This field is required"
|
||||
},
|
||||
"INBOX_NAME": {
|
||||
"LABEL": "Име на входящата кутия",
|
||||
"PLACEHOLDER": "Please enter a inbox name",
|
||||
"ERROR": "This field is required"
|
||||
},
|
||||
"PHONE_NUMBER": {
|
||||
"LABEL": "Телефон",
|
||||
"PLACEHOLDER": "Please enter the phone number from which message will be sent.",
|
||||
"ERROR": "Please enter a valid value. Phone number should start with `+` sign."
|
||||
},
|
||||
"SUBMIT_BUTTON": "Create Bandwidth Channel",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "We were not able to authenticate Bandwidth credentials, please try again"
|
||||
},
|
||||
"API_CALLBACK": {
|
||||
"TITLE": "Callback URL",
|
||||
"SUBTITLE": "You have to configure the message callback URL in Bandwidth with the URL mentioned here."
|
||||
}
|
||||
}
|
||||
},
|
||||
"WHATSAPP": {
|
||||
"TITLE": "WhatsApp Channel",
|
||||
@@ -305,6 +353,14 @@
|
||||
"ENABLED": "Enabled",
|
||||
"DISABLED": "Disabled"
|
||||
},
|
||||
"ALLOW_MESSAGES_AFTER_RESOLVED": {
|
||||
"ENABLED": "Включен",
|
||||
"DISABLED": "Изключен"
|
||||
},
|
||||
"ENABLE_CONTINUITY_VIA_EMAIL": {
|
||||
"ENABLED": "Включен",
|
||||
"DISABLED": "Изключен"
|
||||
},
|
||||
"ENABLE_HMAC": {
|
||||
"LABEL": "Enable"
|
||||
}
|
||||
@@ -351,6 +407,8 @@
|
||||
"AUTO_ASSIGNMENT": "Enable auto assignment",
|
||||
"ENABLE_CSAT": "Enable CSAT",
|
||||
"ENABLE_CSAT_SUB_TEXT": "Enable/Disable CSAT(Customer satisfaction) survey after resolving a conversation",
|
||||
"ENABLE_CONTINUITY_VIA_EMAIL": "Enable conversation continuity via email",
|
||||
"ENABLE_CONTINUITY_VIA_EMAIL_SUB_TEXT": "Conversations will continue over email if the contact email address is available.",
|
||||
"INBOX_UPDATE_TITLE": "Inbox Settings",
|
||||
"INBOX_UPDATE_SUB_TEXT": "Update your inbox settings",
|
||||
"AUTO_ASSIGNMENT_SUB_TEXT": "Enable or disable the automatic assignment of new conversations to the agents added to this inbox.",
|
||||
@@ -361,7 +419,9 @@
|
||||
"INBOX_IDENTIFIER": "Inbox Identifier",
|
||||
"INBOX_IDENTIFIER_SUB_TEXT": "Use the `inbox_identifier` token shown here to authentication your API clients.",
|
||||
"FORWARD_EMAIL_TITLE": "Forward to Email",
|
||||
"FORWARD_EMAIL_SUB_TEXT": "Start forwarding your emails to the following email address."
|
||||
"FORWARD_EMAIL_SUB_TEXT": "Start forwarding your emails to the following email address.",
|
||||
"ALLOW_MESSAGES_AFTER_RESOLVED": "Allow messages after conversation resolved",
|
||||
"ALLOW_MESSAGES_AFTER_RESOLVED_SUB_TEXT": "Allow the end-users to send messages even after the conversation is resolved."
|
||||
},
|
||||
"FACEBOOK_REAUTHORIZE": {
|
||||
"TITLE": "Reauthorize",
|
||||
@@ -461,7 +521,11 @@
|
||||
"DOMAIN": {
|
||||
"LABEL": "Domain",
|
||||
"PLACE_HOLDER": "Domain"
|
||||
}
|
||||
},
|
||||
"ENCRYPTION": "Encryption",
|
||||
"SSL_TLS": "SSL/TLS",
|
||||
"START_TLS": "STARTTLS",
|
||||
"OPEN_SSL_VERIFY_MODE": "Open SSL Verify Mode"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
49
app/javascript/dashboard/i18n/locale/bg/index.js
Normal file
49
app/javascript/dashboard/i18n/locale/bg/index.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import { default as _advancedFilters } from './advancedFilters.json';
|
||||
import { default as _agentMgmt } from './agentMgmt.json';
|
||||
import { default as _attributesMgmt } from './attributesMgmt.json';
|
||||
import { default as _automation } from './automation.json';
|
||||
import { default as _campaign } from './campaign.json';
|
||||
import { default as _cannedMgmt } from './cannedMgmt.json';
|
||||
import { default as _chatlist } from './chatlist.json';
|
||||
import { default as _contact } from './contact.json';
|
||||
import { default as _contactFilters } from './contactFilters.json';
|
||||
import { default as _conversation } from './conversation.json';
|
||||
import { default as _csatMgmtMgmt } from './csatMgmt.json';
|
||||
import { default as _generalSettings } from './generalSettings.json';
|
||||
import { default as _inboxMgmt } from './inboxMgmt.json';
|
||||
import { default as _integrationApps } from './integrationApps.json';
|
||||
import { default as _integrations } from './integrations.json';
|
||||
import { default as _labelsMgmt } from './labelsMgmt.json';
|
||||
import { default as _login } from './login.json';
|
||||
import { default as _report } from './report.json';
|
||||
import { default as _resetPassword } from './resetPassword.json';
|
||||
import { default as _setNewPassword } from './setNewPassword.json';
|
||||
import { default as _settings } from './settings.json';
|
||||
import { default as _signup } from './signup.json';
|
||||
import { default as _teamsSettings } from './teamsSettings.json';
|
||||
|
||||
export default {
|
||||
..._advancedFilters,
|
||||
..._agentMgmt,
|
||||
..._attributesMgmt,
|
||||
..._automation,
|
||||
..._campaign,
|
||||
..._cannedMgmt,
|
||||
..._chatlist,
|
||||
..._contact,
|
||||
..._contactFilters,
|
||||
..._conversation,
|
||||
..._csatMgmtMgmt,
|
||||
..._generalSettings,
|
||||
..._inboxMgmt,
|
||||
..._integrationApps,
|
||||
..._integrations,
|
||||
..._labelsMgmt,
|
||||
..._login,
|
||||
..._report,
|
||||
..._resetPassword,
|
||||
..._setNewPassword,
|
||||
..._settings,
|
||||
..._signup,
|
||||
..._teamsSettings,
|
||||
};
|
||||
@@ -19,6 +19,18 @@
|
||||
"TITLE": "Profile",
|
||||
"NOTE": "Your email address is your identity and is used to log in."
|
||||
},
|
||||
"MESSAGE_SIGNATURE_SECTION": {
|
||||
"TITLE": "Personal message signature",
|
||||
"NOTE": "Create a personal message signature that would be added to all the messages you send from the platform. Use the rich content editor to create a highly personalised signature.",
|
||||
"BTN_TEXT": "Save message signature",
|
||||
"API_ERROR": "Couldn't save signature! Try again",
|
||||
"API_SUCCESS": "Signature saved successfully"
|
||||
},
|
||||
"MESSAGE_SIGNATURE": {
|
||||
"LABEL": "Message Signature",
|
||||
"ERROR": "Message Signature cannot be empty",
|
||||
"PLACEHOLDER": "Insert your personal message signature here."
|
||||
},
|
||||
"PASSWORD_SECTION": {
|
||||
"TITLE": "Password",
|
||||
"NOTE": "Updating your password would reset your logins in multiple devices.",
|
||||
@@ -89,14 +101,14 @@
|
||||
"PLACEHOLDER": "Please enter the current password"
|
||||
},
|
||||
"PASSWORD": {
|
||||
"LABEL": "Password",
|
||||
"LABEL": "New password",
|
||||
"ERROR": "Please enter a password of length 6 or more",
|
||||
"PLACEHOLDER": "Please enter a new password"
|
||||
},
|
||||
"PASSWORD_CONFIRMATION": {
|
||||
"LABEL": "Confirm new password",
|
||||
"ERROR": "Confirm password should match the password",
|
||||
"PLACEHOLDER": "Please re-enter your password"
|
||||
"PLACEHOLDER": "Please re-enter your new password"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -153,6 +165,8 @@
|
||||
"CUSTOM_ATTRIBUTES": "Персонализирани атрибути",
|
||||
"AUTOMATION": "Автоматизация",
|
||||
"TEAMS": "Teams",
|
||||
"CUSTOM_VIEWS_FOLDER": "Folders",
|
||||
"CUSTOM_VIEWS_SEGMENTS": "Segments",
|
||||
"ALL_CONTACTS": "All Contacts",
|
||||
"TAGGED_WITH": "Tagged with",
|
||||
"NEW_LABEL": "New label",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"TEAMS_SETTINGS": {
|
||||
"NEW_TEAM": "Create new team",
|
||||
"HEADER": "Teams",
|
||||
"SIDEBAR_TXT": "<p><b>Teams</b></p> <p>Teams let you organize your agents into groups based on their responsibilities. <br /> A user can be part of multiple teams. You can assign conversations to a team when you are working collaboratively. </p>",
|
||||
"SIDEBAR_TXT": "<p><b>Teams</b></p> <p>Teams let you organize your agents into groups based on their responsibilities. <br /> An agent can be part of multiple teams. You can assign conversations to a team when you are working collaboratively. </p>",
|
||||
"LIST": {
|
||||
"404": "There are no teams created on this account.",
|
||||
"EDIT_TEAM": "Edit team"
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
"contains": "Contains",
|
||||
"does_not_contain": "Does not contain",
|
||||
"is_present": "Is present",
|
||||
"is_not_present": "Is not present"
|
||||
"is_not_present": "Is not present",
|
||||
"is_greater_than": "Is greater than",
|
||||
"is_lesser_than": "Is lesser than"
|
||||
},
|
||||
"ATTRIBUTES": {
|
||||
"STATUS": "Estat",
|
||||
@@ -31,7 +33,54 @@
|
||||
"LABELS": "Etiquetes",
|
||||
"BROWSER_LANGUAGE": "Browser Language",
|
||||
"COUNTRY_NAME": "Country Name",
|
||||
"REFERER_LINK": "Referer link"
|
||||
"REFERER_LINK": "Referer link",
|
||||
"CUSTOM_ATTRIBUTE_LIST": "List",
|
||||
"CUSTOM_ATTRIBUTE_TEXT": "Text",
|
||||
"CUSTOM_ATTRIBUTE_NUMBER": "Number",
|
||||
"CUSTOM_ATTRIBUTE_LINK": "Link",
|
||||
"CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox"
|
||||
},
|
||||
"GROUPS": {
|
||||
"STANDARD_FILTERS": "Standard Filters",
|
||||
"ADDITIONAL_FILTERS": "Additional Filters",
|
||||
"CUSTOM_ATTRIBUTES": "Atributs personalitzats"
|
||||
},
|
||||
"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",
|
||||
"SAVE_BUTTON": "Save filter",
|
||||
"CANCEL_BUTTON": "Cancel·la",
|
||||
"API_FOLDERS": {
|
||||
"SUCCESS_MESSAGE": "Folder created successfully",
|
||||
"ERROR_MESSAGE": "Error while creating folder"
|
||||
},
|
||||
"API_SEGMENTS": {
|
||||
"SUCCESS_MESSAGE": "Segment created successfully",
|
||||
"ERROR_MESSAGE": "Error while creating segment"
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
"DELETE_BUTTON": "Delete filter",
|
||||
"MODAL": {
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirma l'esborrat",
|
||||
"MESSAGE": "Are you sure to delete the filter ",
|
||||
"YES": "Si, esborra",
|
||||
"NO": "No, manten-la"
|
||||
}
|
||||
},
|
||||
"API_FOLDERS": {
|
||||
"SUCCESS_MESSAGE": "Folder deleted successfully",
|
||||
"ERROR_MESSAGE": "Error while deleting folder"
|
||||
},
|
||||
"API_SEGMENTS": {
|
||||
"SUCCESS_MESSAGE": "Segment deleted successfully",
|
||||
"ERROR_MESSAGE": "Error while deleting segment"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,12 +90,12 @@
|
||||
}
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": "No results found."
|
||||
"NO_RESULTS": "No s'ha trobat agents."
|
||||
},
|
||||
"MULTI_SELECTOR": {
|
||||
"PLACEHOLDER": "None",
|
||||
"PLACEHOLDER": "Ningú",
|
||||
"TITLE": {
|
||||
"AGENT": "Select agent",
|
||||
"AGENT": "Seleccionar Agent",
|
||||
"TEAM": "Select team"
|
||||
},
|
||||
"SEARCH": {
|
||||
|
||||
@@ -1,6 +1,89 @@
|
||||
{
|
||||
"AUTOMATION": {
|
||||
"HEADER": "Automation",
|
||||
"HEADER_BTN_TXT": "Add Automation Rule"
|
||||
"HEADER_BTN_TXT": "Add Automation Rule",
|
||||
"LOADING": "Fetching automation rules",
|
||||
"SIDEBAR_TXT": "<p><b>Automation Rules</b> <p>Automation can replace and automate existing processes that require manual effort. You can do many things with automation, including adding labels and assigning conversation to the best agent. So the team focuses on what they do best and spends more little time on manual tasks.</p>",
|
||||
"ADD": {
|
||||
"TITLE": "Add Automation Rule",
|
||||
"SUBMIT": "Crear",
|
||||
"CANCEL_BUTTON_TEXT": "Cancel·la",
|
||||
"FORM": {
|
||||
"NAME": {
|
||||
"LABEL": "Rule Name",
|
||||
"PLACEHOLDER": "Enter rule name",
|
||||
"ERROR": "Name is required"
|
||||
},
|
||||
"DESC": {
|
||||
"LABEL": "Descripció",
|
||||
"PLACEHOLDER": "Enter rule description",
|
||||
"ERROR": "Description is required"
|
||||
},
|
||||
"EVENT": {
|
||||
"LABEL": "Event",
|
||||
"PLACEHOLDER": "Please select one",
|
||||
"ERROR": "Event is required"
|
||||
},
|
||||
"CONDITIONS": {
|
||||
"LABEL": "Conditions"
|
||||
},
|
||||
"ACTIONS": {
|
||||
"LABEL": "Accions"
|
||||
}
|
||||
},
|
||||
"CONDITION_BUTTON_LABEL": "Add Condition",
|
||||
"ACTION_BUTTON_LABEL": "Add Action",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Automation rule added successfully",
|
||||
"ERROR_MESSAGE": "Could not able to create a automation rule, Please try again later"
|
||||
}
|
||||
},
|
||||
"LIST": {
|
||||
"TABLE_HEADER": [
|
||||
"Nom",
|
||||
"Descripció",
|
||||
"Active",
|
||||
"Created on"
|
||||
],
|
||||
"404": "No automation rules found"
|
||||
},
|
||||
"DELETE": {
|
||||
"TITLE": "Delete Automation Rule",
|
||||
"SUBMIT": "Esborrar",
|
||||
"CANCEL_BUTTON_TEXT": "Cancel·la",
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirma l'esborrat",
|
||||
"MESSAGE": "N'estas segur? ",
|
||||
"YES": "Si, esborra ",
|
||||
"NO": "No, segueix "
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Automation rule deleted successfully",
|
||||
"ERROR_MESSAGE": "Could not able to delete a automation rule, Please try again later"
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
"TITLE": "Edit Automation Rule",
|
||||
"SUBMIT": "Edita",
|
||||
"CANCEL_BUTTON_TEXT": "Cancel·la",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Automation rule updated successfully",
|
||||
"ERROR_MESSAGE": "Could not update automation rule, Please try again later"
|
||||
}
|
||||
},
|
||||
"CLONE": {
|
||||
"TOOLTIP": "Clone",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Automation cloned successfully",
|
||||
"ERROR_MESSAGE": "Could not clone automation rule, Please try again later"
|
||||
}
|
||||
},
|
||||
"FORM": {
|
||||
"EDIT": "Edita",
|
||||
"CREATE": "Crear",
|
||||
"DELETE": "Esborrar",
|
||||
"CANCEL": "Cancel·la",
|
||||
"RESET_MESSAGE": "Changing event type will reset the conditions and events you have added below"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"HEADER_BTN_TXT": "Afegeix una resposta predeterminada",
|
||||
"LOADING": "S'estan recollint les respostes predeterminades",
|
||||
"SEARCH_404": "No hi ha cap resposta que coincideixi amb aquesta consulta",
|
||||
"SIDEBAR_TXT": "<p><b>Respostes predeterminades</b> </p><p> Les respostes predeterminades són plantilles de resposta que es poden utilitzar per enviar ràpidament una resposta a una conversa .</p><p> Per crear una Resposta Predeterminada, clica en <b>Afegir Resposta Predeterminada</b>. També pots editar o suprimir una resposta predeterminada fent clic al botó Edita o Suprimeix </p><p> Les respostes predeterminades s'utilitzen amb l'ajuda dels <b>Codi curt</b>. Els agents poden accedir a les respostes predeterminades en un xat escrivint <b>'/'</b> seguit del codi curt. </p>",
|
||||
"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>",
|
||||
"LIST": {
|
||||
"404": "No hi ha respostes predeterminades disponibles en aquest compte.",
|
||||
"TITLE": "Gestiona les respostes predeterminades",
|
||||
@@ -17,12 +17,12 @@
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Afegeix Resposta Predeterminada",
|
||||
"DESC": "Les respostes predeterminades són plantilles de resposta que es poden utilitzar per enviar ràpidament les respostes a les converses.",
|
||||
"DESC": "Canned Responses are saved reply templates which can be used to quickly send out reply to conversation.",
|
||||
"CANCEL_BUTTON_TEXT": "Cancel·la",
|
||||
"FORM": {
|
||||
"SHORT_CODE": {
|
||||
"LABEL": "Codi curt",
|
||||
"PLACEHOLDER": "Introduïu un codi curt",
|
||||
"PLACEHOLDER": "Please enter a short code",
|
||||
"ERROR": "És necessari el codi curt"
|
||||
},
|
||||
"CONTENT": {
|
||||
|
||||
@@ -169,6 +169,7 @@
|
||||
"SUBMIT": "Send message",
|
||||
"CANCEL": "Cancel·la",
|
||||
"SUCCESS_MESSAGE": "Message sent!",
|
||||
"GO_TO_CONVERSATION": "Veure",
|
||||
"ERROR_MESSAGE": "Couldn't send! try again"
|
||||
}
|
||||
},
|
||||
@@ -178,6 +179,8 @@
|
||||
"SEARCH_BUTTON": "Cercar",
|
||||
"SEARCH_INPUT_PLACEHOLDER": "Cerca de contactes",
|
||||
"FILTER_CONTACTS": "Filter",
|
||||
"FILTER_CONTACTS_SAVE": "Save filter",
|
||||
"FILTER_CONTACTS_DELETE": "Delete filter",
|
||||
"LIST": {
|
||||
"LOADING_MESSAGE": "Carregant contactes...",
|
||||
"404": "No hi ha cap contacte que coincideixi amb la vostra cerca 🔍",
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
"contains": "Contains",
|
||||
"does_not_contain": "Does not contain",
|
||||
"is_present": "Is present",
|
||||
"is_not_present": "Is not present"
|
||||
"is_not_present": "Is not present",
|
||||
"is_greater_than": "Is greater than",
|
||||
"is_lesser_than": "Is lesser than"
|
||||
},
|
||||
"ATTRIBUTES": {
|
||||
"NAME": "Nom",
|
||||
@@ -28,7 +30,17 @@
|
||||
"PHONE_NUMBER": "Número de telèfon",
|
||||
"IDENTIFIER": "Identifier",
|
||||
"CITY": "City",
|
||||
"COUNTRY": "Country"
|
||||
"COUNTRY": "Country",
|
||||
"CUSTOM_ATTRIBUTE_LIST": "List",
|
||||
"CUSTOM_ATTRIBUTE_TEXT": "Text",
|
||||
"CUSTOM_ATTRIBUTE_NUMBER": "Number",
|
||||
"CUSTOM_ATTRIBUTE_LINK": "Link",
|
||||
"CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox"
|
||||
},
|
||||
"GROUPS": {
|
||||
"STANDARD_FILTERS": "Standard Filters",
|
||||
"ADDITIONAL_FILTERS": "Additional Filters",
|
||||
"CUSTOM_ATTRIBUTES": "Atributs personalitzats"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user