Compare commits
173
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2fbb93698 | ||
|
|
38d174bda6 | ||
|
|
7ca5d365ae | ||
|
|
6be85a40f7 | ||
|
|
922c989574 | ||
|
|
59928bd85c | ||
|
|
661e905dbd | ||
|
|
270f26e471 | ||
|
|
53fce7be03 | ||
|
|
60a1e9b15d | ||
|
|
65312744c7 | ||
|
|
46c55a53bd | ||
|
|
176e09cde7 | ||
|
|
b09c6b4aac | ||
|
|
96e18d74c5 | ||
|
|
311093f0fb | ||
|
|
54514d9d21 | ||
|
|
4633d2a179 | ||
|
|
73c2690526 | ||
|
|
25b545200e | ||
|
|
23c35c8886 | ||
|
|
3f801fda2c | ||
|
|
51b9fd8eca | ||
|
|
4dc7a653eb | ||
|
|
ea7c5798ff | ||
|
|
1271459874 | ||
|
|
81098b02a8 | ||
|
|
18d6a51d6b | ||
|
|
9c9c1c301d | ||
|
|
5fb40e0b2e | ||
|
|
9ba51a3fd4 | ||
|
|
05d83ce704 | ||
|
|
36b7a86bf7 | ||
|
|
e6d336f727 | ||
|
|
5ab913f7b5 | ||
|
|
feff61755f | ||
|
|
c98c255ed0 | ||
|
|
446a219cd1 | ||
|
|
94829aea8d | ||
|
|
0c4e21bc6a | ||
|
|
a582b8284d | ||
|
|
bbcbc6d52b | ||
|
|
86cb4fd809 | ||
|
|
b434279422 | ||
|
|
d9900e50a0 | ||
|
|
97f1825a14 | ||
|
|
28e837e8f7 | ||
|
|
f4ea90c2dc | ||
|
|
1d9a28ad64 | ||
|
|
f36a3a2e19 | ||
|
|
a14df55210 | ||
|
|
b3633d5c08 | ||
|
|
5ed84af2e3 | ||
|
|
b52caf0341 | ||
|
|
df4de508e7 | ||
|
|
e37d61f836 | ||
|
|
1230d1f251 | ||
|
|
62b36d4aec | ||
|
|
75c57ad039 | ||
|
|
6475a6a593 | ||
|
|
441cc065ae | ||
|
|
0b74be82a3 | ||
|
|
cbc2807296 | ||
|
|
c09e875c83 | ||
|
|
87313ecc35 | ||
|
|
8262123481 | ||
|
|
420be64c45 | ||
|
|
9acb0d86b5 | ||
|
|
2a5ecf84a1 | ||
|
|
286e3a449d | ||
|
|
e5ee6027b4 | ||
|
|
eb412b67bd | ||
|
|
6fb762f96c | ||
|
|
ab1ba1c4c7 | ||
|
|
d276025419 | ||
|
|
94b7154926 | ||
|
|
60951b45fd | ||
|
|
69ad953ae6 | ||
|
|
c4b076dbb2 | ||
|
|
dc49ae2515 | ||
|
|
b71a0da10d | ||
|
|
bb9e3a5495 | ||
|
|
647a65d481 | ||
|
|
99ec18c95b | ||
|
|
64ba23688b | ||
|
|
10673a8d68 | ||
|
|
d488b8ced6 | ||
|
|
0381bdac96 | ||
|
|
87fee33039 | ||
|
|
fe68542547 | ||
|
|
2e1529c755 | ||
|
|
eb20e39b11 | ||
|
|
2fec2e5993 | ||
|
|
b771652656 | ||
|
|
e6d720082c | ||
|
|
d9a0d4eee0 | ||
|
|
2a0f2a8b1e | ||
|
|
aedb59ea67 | ||
|
|
4e2abbaa3d | ||
|
|
9db096f046 | ||
|
|
59b2679641 | ||
|
|
b1d26887f0 | ||
|
|
89a748eef6 | ||
|
|
c00230c8af | ||
|
|
2f6d581384 | ||
|
|
468ef34f85 | ||
|
|
7f4005cdcc | ||
|
|
57fdfe26f0 | ||
|
|
88ba4e4562 | ||
|
|
394ba4ff73 | ||
|
|
c0d1efa0ad | ||
|
|
dfc80e6f20 | ||
|
|
187b435eb9 | ||
|
|
1fce8d1183 | ||
|
|
c51d2d3c93 | ||
|
|
4450bebf35 | ||
|
|
b3ce7bd278 | ||
|
|
345a212447 | ||
|
|
83450b7c24 | ||
|
|
b58ec24d81 | ||
|
|
5e799e7e6b | ||
|
|
01ad380822 | ||
|
|
84d2c0fd25 | ||
|
|
87212d3b6e | ||
|
|
4ca6ec1812 | ||
|
|
4452d84097 | ||
|
|
9a81927154 | ||
|
|
daed6eb1e5 | ||
|
|
673669cd25 | ||
|
|
bdd5a77f45 | ||
|
|
92150cb7c8 | ||
|
|
e52becbc40 | ||
|
|
7f3d4aaaa7 | ||
|
|
7748ef2787 | ||
|
|
f2d9895597 | ||
|
|
db4e8588cd | ||
|
|
0c05599282 | ||
|
|
00e1f50458 | ||
|
|
ee89943aa2 | ||
|
|
9dc6b94537 | ||
|
|
e9816f8d3d | ||
|
|
51d1c501cf | ||
|
|
1539a8bc36 | ||
|
|
ffa8c0ce8a | ||
|
|
4762edcba3 | ||
|
|
1bd2da4832 | ||
|
|
b923bfecca | ||
|
|
46e758cc84 | ||
|
|
9165cab1ec | ||
|
|
bda09cc064 | ||
|
|
408cc4dd3f | ||
|
|
c6b636baee | ||
|
|
bc24d1f02c | ||
|
|
62a7cfa188 | ||
|
|
c4fa5cd4d9 | ||
|
|
d40fa27289 | ||
|
|
c789ef555c | ||
|
|
0674edf81b | ||
|
|
9362d126aa | ||
|
|
35797bd393 | ||
|
|
68ff52c432 | ||
|
|
5d2afc065f | ||
|
|
cd8f245483 | ||
|
|
735979c834 | ||
|
|
5cd6996d5e | ||
|
|
ff49996950 | ||
|
|
14417ef14f | ||
|
|
1393ad5143 | ||
|
|
4f262fc041 | ||
|
|
aa639dd78e | ||
|
|
aa9c0b9f6b | ||
|
|
526d6e2a0a | ||
|
|
96746cc592 |
@@ -94,3 +94,4 @@ yarn-debug.log*
|
|||||||
.vscode
|
.vscode
|
||||||
.claude/settings.local.json
|
.claude/settings.local.json
|
||||||
.cursor
|
.cursor
|
||||||
|
CLAUDE.local.md
|
||||||
|
|||||||
@@ -121,6 +121,8 @@ gem 'sentry-sidekiq', '>= 5.19.0', require: false
|
|||||||
gem 'sidekiq', '>= 7.3.1'
|
gem 'sidekiq', '>= 7.3.1'
|
||||||
# We want cron jobs
|
# We want cron jobs
|
||||||
gem 'sidekiq-cron', '>= 1.12.0'
|
gem 'sidekiq-cron', '>= 1.12.0'
|
||||||
|
# for sidekiq healthcheck
|
||||||
|
gem 'sidekiq_alive'
|
||||||
|
|
||||||
##-- Push notification service --##
|
##-- Push notification service --##
|
||||||
gem 'fcm'
|
gem 'fcm'
|
||||||
@@ -177,6 +179,8 @@ gem 'reverse_markdown'
|
|||||||
|
|
||||||
gem 'iso-639'
|
gem 'iso-639'
|
||||||
gem 'ruby-openai'
|
gem 'ruby-openai'
|
||||||
|
gem 'ai-agents', '>= 0.4.3'
|
||||||
|
gem 'ruby_llm-schema'
|
||||||
|
|
||||||
gem 'shopify_api'
|
gem 'shopify_api'
|
||||||
|
|
||||||
|
|||||||
+25
-6
@@ -126,6 +126,8 @@ GEM
|
|||||||
jbuilder (~> 2)
|
jbuilder (~> 2)
|
||||||
rails (>= 4.2, < 7.2)
|
rails (>= 4.2, < 7.2)
|
||||||
selectize-rails (~> 0.6)
|
selectize-rails (~> 0.6)
|
||||||
|
ai-agents (0.4.3)
|
||||||
|
ruby_llm (~> 1.3)
|
||||||
annotate (3.2.0)
|
annotate (3.2.0)
|
||||||
activerecord (>= 3.2, < 8.0)
|
activerecord (>= 3.2, < 8.0)
|
||||||
rake (>= 10.4, < 14.0)
|
rake (>= 10.4, < 14.0)
|
||||||
@@ -357,6 +359,7 @@ GEM
|
|||||||
grpc (1.72.0-x86_64-linux)
|
grpc (1.72.0-x86_64-linux)
|
||||||
google-protobuf (>= 3.25, < 5.0)
|
google-protobuf (>= 3.25, < 5.0)
|
||||||
googleapis-common-protos-types (~> 1.0)
|
googleapis-common-protos-types (~> 1.0)
|
||||||
|
gserver (0.0.1)
|
||||||
haikunator (1.1.1)
|
haikunator (1.1.1)
|
||||||
hairtrigger (1.0.0)
|
hairtrigger (1.0.0)
|
||||||
activerecord (>= 6.0, < 8)
|
activerecord (>= 6.0, < 8)
|
||||||
@@ -475,7 +478,7 @@ GEM
|
|||||||
mime-types-data (3.2023.0218.1)
|
mime-types-data (3.2023.0218.1)
|
||||||
mini_magick (4.12.0)
|
mini_magick (4.12.0)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.8)
|
mini_portile2 (2.8.9)
|
||||||
minitest (5.25.5)
|
minitest (5.25.5)
|
||||||
mock_redis (0.36.0)
|
mock_redis (0.36.0)
|
||||||
ruby2_keywords
|
ruby2_keywords
|
||||||
@@ -506,14 +509,14 @@ GEM
|
|||||||
newrelic_rpm (9.6.0)
|
newrelic_rpm (9.6.0)
|
||||||
base64
|
base64
|
||||||
nio4r (2.7.3)
|
nio4r (2.7.3)
|
||||||
nokogiri (1.18.8)
|
nokogiri (1.18.9)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.8-arm64-darwin)
|
nokogiri (1.18.9-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.8-x86_64-darwin)
|
nokogiri (1.18.9-x86_64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.8-x86_64-linux-gnu)
|
nokogiri (1.18.9-x86_64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
oauth (1.1.0)
|
oauth (1.1.0)
|
||||||
oauth-tty (~> 1.0, >= 1.0.1)
|
oauth-tty (~> 1.0, >= 1.0.1)
|
||||||
@@ -717,6 +720,16 @@ GEM
|
|||||||
ruby2ruby (2.5.0)
|
ruby2ruby (2.5.0)
|
||||||
ruby_parser (~> 3.1)
|
ruby_parser (~> 3.1)
|
||||||
sexp_processor (~> 4.6)
|
sexp_processor (~> 4.6)
|
||||||
|
ruby_llm (1.5.1)
|
||||||
|
base64
|
||||||
|
event_stream_parser (~> 1)
|
||||||
|
faraday (>= 1.10.0)
|
||||||
|
faraday-multipart (>= 1)
|
||||||
|
faraday-net_http (>= 1)
|
||||||
|
faraday-retry (>= 1)
|
||||||
|
marcel (~> 1.0)
|
||||||
|
zeitwerk (~> 2)
|
||||||
|
ruby_llm-schema (0.1.0)
|
||||||
ruby_parser (3.20.0)
|
ruby_parser (3.20.0)
|
||||||
sexp_processor (~> 4.16)
|
sexp_processor (~> 4.16)
|
||||||
sass (3.7.4)
|
sass (3.7.4)
|
||||||
@@ -774,6 +787,9 @@ GEM
|
|||||||
fugit (~> 1.8)
|
fugit (~> 1.8)
|
||||||
globalid (>= 1.0.1)
|
globalid (>= 1.0.1)
|
||||||
sidekiq (>= 6)
|
sidekiq (>= 6)
|
||||||
|
sidekiq_alive (2.5.0)
|
||||||
|
gserver (~> 0.0.1)
|
||||||
|
sidekiq (>= 5, < 9)
|
||||||
signet (0.17.0)
|
signet (0.17.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
faraday (>= 0.17.5, < 3.a)
|
faraday (>= 0.17.5, < 3.a)
|
||||||
@@ -812,7 +828,7 @@ GEM
|
|||||||
stripe (8.5.0)
|
stripe (8.5.0)
|
||||||
telephone_number (1.4.20)
|
telephone_number (1.4.20)
|
||||||
test-prof (1.2.1)
|
test-prof (1.2.1)
|
||||||
thor (1.3.1)
|
thor (1.4.0)
|
||||||
tilt (2.3.0)
|
tilt (2.3.0)
|
||||||
time_diff (0.3.0)
|
time_diff (0.3.0)
|
||||||
activesupport
|
activesupport
|
||||||
@@ -895,6 +911,7 @@ DEPENDENCIES
|
|||||||
administrate (>= 0.20.1)
|
administrate (>= 0.20.1)
|
||||||
administrate-field-active_storage (>= 1.0.3)
|
administrate-field-active_storage (>= 1.0.3)
|
||||||
administrate-field-belongs_to_search (>= 0.9.0)
|
administrate-field-belongs_to_search (>= 0.9.0)
|
||||||
|
ai-agents (>= 0.4.3)
|
||||||
annotate
|
annotate
|
||||||
attr_extras
|
attr_extras
|
||||||
audited (~> 5.4, >= 5.4.1)
|
audited (~> 5.4, >= 5.4.1)
|
||||||
@@ -988,6 +1005,7 @@ DEPENDENCIES
|
|||||||
rubocop-rails
|
rubocop-rails
|
||||||
rubocop-rspec
|
rubocop-rspec
|
||||||
ruby-openai
|
ruby-openai
|
||||||
|
ruby_llm-schema
|
||||||
scout_apm
|
scout_apm
|
||||||
scss_lint
|
scss_lint
|
||||||
seed_dump
|
seed_dump
|
||||||
@@ -998,6 +1016,7 @@ DEPENDENCIES
|
|||||||
shoulda-matchers
|
shoulda-matchers
|
||||||
sidekiq (>= 7.3.1)
|
sidekiq (>= 7.3.1)
|
||||||
sidekiq-cron (>= 1.12.0)
|
sidekiq-cron (>= 1.12.0)
|
||||||
|
sidekiq_alive
|
||||||
simplecov (= 0.17.1)
|
simplecov (= 0.17.1)
|
||||||
slack-ruby-client (~> 2.5.2)
|
slack-ruby-client (~> 2.5.2)
|
||||||
spring
|
spring
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
3.13.0
|
4.4.0
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
3.2.0
|
3.4.2
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
# We don't want to update the name of the identified original contact.
|
# We don't want to update the name of the identified original contact.
|
||||||
|
|
||||||
class ContactIdentifyAction
|
class ContactIdentifyAction
|
||||||
|
include UrlHelper
|
||||||
pattr_initialize [:contact!, :params!, { retain_original_contact_name: false, discard_invalid_attrs: false }]
|
pattr_initialize [:contact!, :params!, { retain_original_contact_name: false, discard_invalid_attrs: false }]
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
@@ -104,7 +105,14 @@ class ContactIdentifyAction
|
|||||||
# TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded
|
# TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded
|
||||||
@contact.discard_invalid_attrs if discard_invalid_attrs
|
@contact.discard_invalid_attrs if discard_invalid_attrs
|
||||||
@contact.save!
|
@contact.save!
|
||||||
Avatar::AvatarFromUrlJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present? && !@contact.avatar.attached?
|
enqueue_avatar_job
|
||||||
|
end
|
||||||
|
|
||||||
|
def enqueue_avatar_job
|
||||||
|
return unless params[:avatar_url].present? && !@contact.avatar.attached?
|
||||||
|
return unless url_valid?(params[:avatar_url])
|
||||||
|
|
||||||
|
Avatar::AvatarFromUrlJob.perform_later(@contact, params[:avatar_url])
|
||||||
end
|
end
|
||||||
|
|
||||||
def merge_contact(base_contact, merge_contact)
|
def merge_contact(base_contact, merge_contact)
|
||||||
|
|||||||
@@ -30,7 +30,14 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def facebook_pages
|
def facebook_pages
|
||||||
@page_details = mark_already_existing_facebook_pages(fb_object.get_connections('me', 'accounts'))
|
pages = []
|
||||||
|
fb_pages = fb_object.get_connections('me', 'accounts')
|
||||||
|
pages.concat(fb_pages)
|
||||||
|
while fb_pages.respond_to?(:next_page) && (next_page = fb_pages.next_page)
|
||||||
|
fb_pages = next_page
|
||||||
|
pages.concat(fb_pages)
|
||||||
|
end
|
||||||
|
@page_details = mark_already_existing_facebook_pages(pages)
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_instagram_id(page_access_token, facebook_channel)
|
def set_instagram_id(page_access_token, facebook_channel)
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||||||
def resolved_contacts
|
def resolved_contacts
|
||||||
return @resolved_contacts if @resolved_contacts
|
return @resolved_contacts if @resolved_contacts
|
||||||
|
|
||||||
@resolved_contacts = Current.account.contacts.resolved_contacts
|
@resolved_contacts = Current.account.contacts.resolved_contacts(use_crm_v2: Current.account.feature_enabled?('crm_v2'))
|
||||||
|
|
||||||
@resolved_contacts = @resolved_contacts.tagged_with(params[:labels], any: true) if params[:labels].present?
|
@resolved_contacts = @resolved_contacts.tagged_with(params[:labels], any: true) if params[:labels].present?
|
||||||
@resolved_contacts
|
@resolved_contacts
|
||||||
|
|||||||
@@ -69,6 +69,17 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
|||||||
render status: :ok, json: { message: I18n.t('messages.inbox_deletetion_response') }
|
render status: :ok, json: { message: I18n.t('messages.inbox_deletetion_response') }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def sync_templates
|
||||||
|
unless @inbox.channel.is_a?(Channel::Whatsapp)
|
||||||
|
return render status: :unprocessable_entity, json: { error: 'Template sync is only available for WhatsApp channels' }
|
||||||
|
end
|
||||||
|
|
||||||
|
Channels::Whatsapp::TemplatesSyncJob.perform_later(@inbox.channel)
|
||||||
|
render status: :ok, json: { message: 'Template sync initiated successfully' }
|
||||||
|
rescue StandardError => e
|
||||||
|
render status: :internal_server_error, json: { error: e.message }
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def fetch_inbox
|
def fetch_inbox
|
||||||
|
|||||||
@@ -47,6 +47,20 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
|||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def send_instructions
|
||||||
|
email = permitted_params[:email]
|
||||||
|
return render_could_not_create_error(I18n.t('portals.send_instructions.email_required')) if email.blank?
|
||||||
|
return render_could_not_create_error(I18n.t('portals.send_instructions.invalid_email_format')) unless valid_email?(email)
|
||||||
|
return render_could_not_create_error(I18n.t('portals.send_instructions.custom_domain_not_configured')) if @portal.custom_domain.blank?
|
||||||
|
|
||||||
|
PortalInstructionsMailer.send_cname_instructions(
|
||||||
|
portal: @portal,
|
||||||
|
recipient_email: email
|
||||||
|
).deliver_later
|
||||||
|
|
||||||
|
render json: { message: I18n.t('portals.send_instructions.instructions_sent_successfully') }, status: :ok
|
||||||
|
end
|
||||||
|
|
||||||
def process_attached_logo
|
def process_attached_logo
|
||||||
blob_id = params[:blob_id]
|
blob_id = params[:blob_id]
|
||||||
blob = ActiveStorage::Blob.find_by(id: blob_id)
|
blob = ActiveStorage::Blob.find_by(id: blob_id)
|
||||||
@@ -60,12 +74,12 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def permitted_params
|
def permitted_params
|
||||||
params.permit(:id)
|
params.permit(:id, :email)
|
||||||
end
|
end
|
||||||
|
|
||||||
def portal_params
|
def portal_params
|
||||||
params.require(:portal).permit(
|
params.require(:portal).permit(
|
||||||
:account_id, :color, :custom_domain, :header_text, :homepage_link,
|
:id, :account_id, :color, :custom_domain, :header_text, :homepage_link,
|
||||||
:name, :page_title, :slug, :archived, { config: [:default_locale, { allowed_locales: [] }] }
|
:name, :page_title, :slug, :archived, { config: [:default_locale, { allowed_locales: [] }] }
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@@ -88,4 +102,10 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
|||||||
domain = URI.parse(@portal.custom_domain)
|
domain = URI.parse(@portal.custom_domain)
|
||||||
domain.is_a?(URI::HTTP) ? domain.host : @portal.custom_domain
|
domain.is_a?(URI::HTTP) ? domain.host : @portal.custom_domain
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def valid_email?(email)
|
||||||
|
ValidEmail2::Address.new(email).valid?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Api::V1::Accounts::PortalsController.prepend_mod_with('Api::V1::Accounts::PortalsController')
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ class MicrosoftController < ApplicationController
|
|||||||
after_action :set_version_header
|
after_action :set_version_header
|
||||||
|
|
||||||
def identity_association
|
def identity_association
|
||||||
microsoft_indentity
|
microsoft_identity
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -11,7 +11,7 @@ class MicrosoftController < ApplicationController
|
|||||||
response.headers['Content-Length'] = { associatedApplications: [{ applicationId: @identity_json }] }.to_json.length
|
response.headers['Content-Length'] = { associatedApplications: [{ applicationId: @identity_json }] }.to_json.length
|
||||||
end
|
end
|
||||||
|
|
||||||
def microsoft_indentity
|
def microsoft_identity
|
||||||
@identity_json = GlobalConfigService.load('AZURE_APP_ID', nil)
|
@identity_json = GlobalConfigService.load('AZURE_APP_ID', nil)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,7 +4,16 @@ class Webhooks::InstagramController < ActionController::API
|
|||||||
def events
|
def events
|
||||||
Rails.logger.info('Instagram webhook received events')
|
Rails.logger.info('Instagram webhook received events')
|
||||||
if params['object'].casecmp('instagram').zero?
|
if params['object'].casecmp('instagram').zero?
|
||||||
::Webhooks::InstagramEventsJob.perform_later(params.to_unsafe_hash[:entry])
|
entry_params = params.to_unsafe_hash[:entry]
|
||||||
|
|
||||||
|
if contains_echo_event?(entry_params)
|
||||||
|
# Add delay to prevent race condition where echo arrives before send message API completes
|
||||||
|
# This avoids duplicate messages when echo comes early during API processing
|
||||||
|
::Webhooks::InstagramEventsJob.set(wait: 2.seconds).perform_later(entry_params)
|
||||||
|
else
|
||||||
|
::Webhooks::InstagramEventsJob.perform_later(entry_params)
|
||||||
|
end
|
||||||
|
|
||||||
render json: :ok
|
render json: :ok
|
||||||
else
|
else
|
||||||
Rails.logger.warn("Message is not received from the instagram webhook event: #{params['object']}")
|
Rails.logger.warn("Message is not received from the instagram webhook event: #{params['object']}")
|
||||||
@@ -14,6 +23,16 @@ class Webhooks::InstagramController < ActionController::API
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def contains_echo_event?(entry_params)
|
||||||
|
return false unless entry_params.is_a?(Array)
|
||||||
|
|
||||||
|
entry_params.any? do |entry|
|
||||||
|
# Check messaging array for echo events
|
||||||
|
messaging_events = entry[:messaging] || []
|
||||||
|
messaging_events.any? { |messaging| messaging.dig(:message, :is_echo).present? }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def valid_token?(token)
|
def valid_token?(token)
|
||||||
# Validates against both IG_VERIFY_TOKEN (Instagram channel via Facebook page) and
|
# Validates against both IG_VERIFY_TOKEN (Instagram channel via Facebook page) and
|
||||||
# INSTAGRAM_VERIFY_TOKEN (Instagram channel via direct Instagram login)
|
# INSTAGRAM_VERIFY_TOKEN (Instagram channel via direct Instagram login)
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ export default {
|
|||||||
<div
|
<div
|
||||||
v-if="!authUIFlags.isFetching && !accountUIFlags.isFetchingItem"
|
v-if="!authUIFlags.isFetching && !accountUIFlags.isFetchingItem"
|
||||||
id="app"
|
id="app"
|
||||||
class="flex-grow-0 w-full h-full min-h-0 app-wrapper"
|
class="flex flex-col w-full h-screen min-h-0"
|
||||||
:class="{ 'app-rtl--wrapper': isRTL }"
|
:class="{ 'app-rtl--wrapper': isRTL }"
|
||||||
:dir="isRTL ? 'rtl' : 'ltr'"
|
:dir="isRTL ? 'rtl' : 'ltr'"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/* global axios */
|
||||||
|
import ApiClient from '../ApiClient';
|
||||||
|
|
||||||
|
class CaptainScenarios extends ApiClient {
|
||||||
|
constructor() {
|
||||||
|
super('captain/assistants', { accountScoped: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
get({ assistantId, page = 1, searchKey } = {}) {
|
||||||
|
return axios.get(`${this.url}/${assistantId}/scenarios`, {
|
||||||
|
params: { page, searchKey },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
show({ assistantId, id }) {
|
||||||
|
return axios.get(`${this.url}/${assistantId}/scenarios/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
create({ assistantId, ...data } = {}) {
|
||||||
|
return axios.post(`${this.url}/${assistantId}/scenarios`, {
|
||||||
|
scenario: data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
update({ assistantId, id }, data = {}) {
|
||||||
|
return axios.put(`${this.url}/${assistantId}/scenarios/${id}`, {
|
||||||
|
scenario: data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
delete({ assistantId, id }) {
|
||||||
|
return axios.delete(`${this.url}/${assistantId}/scenarios/${id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new CaptainScenarios();
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/* global axios */
|
||||||
|
import ApiClient from '../ApiClient';
|
||||||
|
|
||||||
|
class CaptainTools extends ApiClient {
|
||||||
|
constructor() {
|
||||||
|
super('captain/assistants/tools', { accountScoped: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
get(params = {}) {
|
||||||
|
return axios.get(this.url, {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new CaptainTools();
|
||||||
@@ -21,6 +21,14 @@ class PortalsAPI extends ApiClient {
|
|||||||
deleteLogo(portalSlug) {
|
deleteLogo(portalSlug) {
|
||||||
return axios.delete(`${this.url}/${portalSlug}/logo`);
|
return axios.delete(`${this.url}/${portalSlug}/logo`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendCnameInstructions(portalSlug, email) {
|
||||||
|
return axios.post(`${this.url}/${portalSlug}/send_instructions`, { email });
|
||||||
|
}
|
||||||
|
|
||||||
|
sslStatus(portalSlug) {
|
||||||
|
return axios.get(`${this.url}/${portalSlug}/ssl_status`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PortalsAPI;
|
export default PortalsAPI;
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ class Inboxes extends CacheEnabledApiClient {
|
|||||||
agent_bot: botId,
|
agent_bot: botId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
syncTemplates(inboxId) {
|
||||||
|
return axios.post(`${this.url}/${inboxId}/sync_templates`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Inboxes();
|
export default new Inboxes();
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ describe('#InboxesAPI', () => {
|
|||||||
expect(inboxesAPI).toHaveProperty('getCampaigns');
|
expect(inboxesAPI).toHaveProperty('getCampaigns');
|
||||||
expect(inboxesAPI).toHaveProperty('getAgentBot');
|
expect(inboxesAPI).toHaveProperty('getAgentBot');
|
||||||
expect(inboxesAPI).toHaveProperty('setAgentBot');
|
expect(inboxesAPI).toHaveProperty('setAgentBot');
|
||||||
|
expect(inboxesAPI).toHaveProperty('syncTemplates');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('API calls', () => {
|
describe('API calls', () => {
|
||||||
@@ -40,5 +41,12 @@ describe('#InboxesAPI', () => {
|
|||||||
inboxesAPI.deleteInboxAvatar(2);
|
inboxesAPI.deleteInboxAvatar(2);
|
||||||
expect(axiosMock.delete).toHaveBeenCalledWith('/api/v1/inboxes/2/avatar');
|
expect(axiosMock.delete).toHaveBeenCalledWith('/api/v1/inboxes/2/avatar');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('#syncTemplates', () => {
|
||||||
|
inboxesAPI.syncTemplates(2);
|
||||||
|
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||||
|
'/api/v1/inboxes/2/sync_templates'
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -37,30 +37,6 @@ body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-wrapper {
|
|
||||||
@apply h-screen flex-grow-0 min-h-0 w-full;
|
|
||||||
|
|
||||||
.button--fixed-top {
|
|
||||||
@apply fixed ltr:right-2 rtl:left-2 top-2 flex flex-row;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner + .app-wrapper {
|
|
||||||
// Reduce the height of the dashboard to make room for the banner.
|
|
||||||
// And causing the top right green-action button to be pushed down when scrolling.
|
|
||||||
@apply h-[calc(100%-48px)];
|
|
||||||
|
|
||||||
.button--fixed-top {
|
|
||||||
@apply top-14;
|
|
||||||
}
|
|
||||||
|
|
||||||
.off-canvas-content {
|
|
||||||
.button--fixed-top {
|
|
||||||
@apply top-2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip {
|
.tooltip {
|
||||||
@apply bg-n-solid-2 text-n-slate-12 py-1 px-2 z-40 text-xs rounded-md max-w-96;
|
@apply bg-n-solid-2 text-n-slate-12 py-1 px-2 z-40 text-xs rounded-md max-w-96;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const props = defineProps({
|
|||||||
enableVariables: { type: Boolean, default: false },
|
enableVariables: { type: Boolean, default: false },
|
||||||
enableCannedResponses: { type: Boolean, default: true },
|
enableCannedResponses: { type: Boolean, default: true },
|
||||||
enabledMenuOptions: { type: Array, default: () => [] },
|
enabledMenuOptions: { type: Array, default: () => [] },
|
||||||
|
enableCaptainTools: { type: Boolean, default: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue']);
|
const emit = defineEmits(['update:modelValue']);
|
||||||
@@ -98,6 +99,7 @@ watch(
|
|||||||
:enable-variables="enableVariables"
|
:enable-variables="enableVariables"
|
||||||
:enable-canned-responses="enableCannedResponses"
|
:enable-canned-responses="enableCannedResponses"
|
||||||
:enabled-menu-options="enabledMenuOptions"
|
:enabled-menu-options="enabledMenuOptions"
|
||||||
|
:enable-captain-tools="enableCaptainTools"
|
||||||
@input="handleInput"
|
@input="handleInput"
|
||||||
@focus="handleFocus"
|
@focus="handleFocus"
|
||||||
@blur="handleBlur"
|
@blur="handleBlur"
|
||||||
|
|||||||
+26
-1
@@ -1,6 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, watch } from 'vue';
|
import { ref, reactive, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useVuelidate } from '@vuelidate/core';
|
||||||
|
import { helpers } from '@vuelidate/validators';
|
||||||
|
import { isValidDomain } from '@chatwoot/utils';
|
||||||
|
|
||||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||||
import Input from 'dashboard/components-next/input/Input.vue';
|
import Input from 'dashboard/components-next/input/Input.vue';
|
||||||
@@ -26,6 +29,20 @@ const formState = reactive({
|
|||||||
customDomain: props.customDomain,
|
customDomain: props.customDomain,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
customDomain: {
|
||||||
|
isValidDomain: helpers.withMessage(
|
||||||
|
() =>
|
||||||
|
t(
|
||||||
|
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DIALOG.FORMAT_ERROR'
|
||||||
|
),
|
||||||
|
isValidDomain
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const v$ = useVuelidate(rules, formState);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.customDomain,
|
() => props.customDomain,
|
||||||
newVal => {
|
newVal => {
|
||||||
@@ -33,7 +50,10 @@ watch(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDialogConfirm = () => {
|
const handleDialogConfirm = async () => {
|
||||||
|
const isFormCorrect = await v$.value.$validate();
|
||||||
|
if (!isFormCorrect) return;
|
||||||
|
|
||||||
emit('addCustomDomain', formState.customDomain);
|
emit('addCustomDomain', formState.customDomain);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -67,6 +87,11 @@ defineExpose({ dialogRef });
|
|||||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DIALOG.PLACEHOLDER'
|
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DIALOG.PLACEHOLDER'
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
:message="
|
||||||
|
v$.customDomain.$error ? v$.customDomain.$errors[0].$message : ''
|
||||||
|
"
|
||||||
|
:message-type="v$.customDomain.$error ? 'error' : 'info'"
|
||||||
|
@blur="v$.customDomain.$touch()"
|
||||||
/>
|
/>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+138
-37
@@ -1,9 +1,15 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue';
|
import { reactive, computed, ref } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
||||||
import { getHostNameFromURL } from 'dashboard/helper/URLHelper';
|
import { getHostNameFromURL } from 'dashboard/helper/URLHelper';
|
||||||
|
import { email, required } from '@vuelidate/validators';
|
||||||
|
import { useVuelidate } from '@vuelidate/core';
|
||||||
|
|
||||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||||
|
import Input from 'dashboard/components-next/input/Input.vue';
|
||||||
|
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
customDomain: {
|
customDomain: {
|
||||||
@@ -12,10 +18,20 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['confirm']);
|
const emit = defineEmits(['send', 'close']);
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
email: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const validationRules = {
|
||||||
|
email: { email, required },
|
||||||
|
};
|
||||||
|
|
||||||
|
const v$ = useVuelidate(validationRules, state);
|
||||||
|
|
||||||
const domain = computed(() => {
|
const domain = computed(() => {
|
||||||
const { hostURL, helpCenterURL } = window?.chatwootConfig || {};
|
const { hostURL, helpCenterURL } = window?.chatwootConfig || {};
|
||||||
return getHostNameFromURL(helpCenterURL) || getHostNameFromURL(hostURL) || '';
|
return getHostNameFromURL(helpCenterURL) || getHostNameFromURL(hostURL) || '';
|
||||||
@@ -25,10 +41,34 @@ const subdomainCNAME = computed(
|
|||||||
() => `${props.customDomain} CNAME ${domain.value}`
|
() => `${props.customDomain} CNAME ${domain.value}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleCopy = async e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
await copyTextToClipboard(subdomainCNAME.value);
|
||||||
|
useAlert(
|
||||||
|
t(
|
||||||
|
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.COPY'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const dialogRef = ref(null);
|
const dialogRef = ref(null);
|
||||||
|
|
||||||
const handleDialogConfirm = () => {
|
const resetForm = () => {
|
||||||
emit('confirm');
|
v$.value.$reset();
|
||||||
|
state.email = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
resetForm();
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSend = async () => {
|
||||||
|
const isFormCorrect = await v$.value.$validate();
|
||||||
|
if (!isFormCorrect) return;
|
||||||
|
|
||||||
|
emit('send', state.email);
|
||||||
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
defineExpose({ dialogRef });
|
defineExpose({ dialogRef });
|
||||||
@@ -37,42 +77,103 @@ defineExpose({ dialogRef });
|
|||||||
<template>
|
<template>
|
||||||
<Dialog
|
<Dialog
|
||||||
ref="dialogRef"
|
ref="dialogRef"
|
||||||
:title="
|
|
||||||
t(
|
|
||||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.HEADER'
|
|
||||||
)
|
|
||||||
"
|
|
||||||
:confirm-button-label="
|
|
||||||
t(
|
|
||||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.CONFIRM_BUTTON_LABEL'
|
|
||||||
)
|
|
||||||
"
|
|
||||||
:show-cancel-button="false"
|
:show-cancel-button="false"
|
||||||
@confirm="handleDialogConfirm"
|
:show-confirm-button="false"
|
||||||
|
@close="resetForm"
|
||||||
>
|
>
|
||||||
<template #description>
|
<NextButton
|
||||||
<p class="mb-0 text-sm text-n-slate-12">
|
icon="i-lucide-x"
|
||||||
{{
|
sm
|
||||||
t(
|
ghost
|
||||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.DESCRIPTION'
|
slate
|
||||||
)
|
class="flex-shrink-0 absolute top-2 ltr:right-2 rtl:left-2"
|
||||||
}}
|
@click="onClose"
|
||||||
</p>
|
/>
|
||||||
</template>
|
<div class="flex flex-col gap-6 divide-y divide-n-strong">
|
||||||
|
<div class="flex flex-col gap-6">
|
||||||
|
<div class="flex flex-col gap-2 ltr:pr-10 rtl:pl-10">
|
||||||
|
<h3 class="text-base font-medium leading-6 text-n-slate-12">
|
||||||
|
{{
|
||||||
|
t(
|
||||||
|
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.HEADER'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</h3>
|
||||||
|
<p class="mb-0 text-sm text-n-slate-12">
|
||||||
|
{{
|
||||||
|
t(
|
||||||
|
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.DESCRIPTION'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3 w-full">
|
||||||
|
<span
|
||||||
|
class="min-h-10 px-3 py-2.5 inline-flex items-center w-full text-sm bg-transparent border rounded-lg text-n-slate-11 border-n-strong"
|
||||||
|
>
|
||||||
|
{{ subdomainCNAME }}
|
||||||
|
</span>
|
||||||
|
<NextButton
|
||||||
|
faded
|
||||||
|
slate
|
||||||
|
type="button"
|
||||||
|
icon="i-lucide-copy"
|
||||||
|
class="flex-shrink-0"
|
||||||
|
@click="handleCopy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-6">
|
<div class="flex flex-col gap-6 pt-6">
|
||||||
<span
|
<div class="flex flex-col gap-2 ltr:pr-10 rtl:pl-10">
|
||||||
class="h-10 px-3 py-2.5 text-sm select-none bg-transparent border rounded-lg text-n-slate-11 border-n-strong"
|
<h3 class="text-base font-medium leading-6 text-n-slate-12">
|
||||||
>
|
{{
|
||||||
{{ subdomainCNAME }}
|
t(
|
||||||
</span>
|
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.SEND_INSTRUCTIONS.HEADER'
|
||||||
<p class="text-sm text-n-slate-12">
|
)
|
||||||
{{
|
}}
|
||||||
t(
|
</h3>
|
||||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.HELP_TEXT'
|
<p class="mb-0 text-sm text-n-slate-12">
|
||||||
)
|
{{
|
||||||
}}
|
t(
|
||||||
</p>
|
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.SEND_INSTRUCTIONS.DESCRIPTION'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
class="flex items-start gap-3 w-full"
|
||||||
|
@submit.prevent="handleSend"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
v-model="state.email"
|
||||||
|
:placeholder="
|
||||||
|
t(
|
||||||
|
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.SEND_INSTRUCTIONS.PLACEHOLDER'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:message="
|
||||||
|
v$.email.$error
|
||||||
|
? t(
|
||||||
|
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.SEND_INSTRUCTIONS.ERROR'
|
||||||
|
)
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
:message-type="v$.email.$error ? 'error' : 'info'"
|
||||||
|
class="w-full"
|
||||||
|
@blur="v$.email.$touch()"
|
||||||
|
/>
|
||||||
|
<NextButton
|
||||||
|
:label="
|
||||||
|
t(
|
||||||
|
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.SEND_INSTRUCTIONS.SEND_BUTTON'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
type="submit"
|
||||||
|
class="flex-shrink-0"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+159
-23
@@ -1,6 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useAccount } from 'dashboard/composables/useAccount';
|
||||||
|
|
||||||
import AddCustomDomainDialog from 'dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/AddCustomDomainDialog.vue';
|
import AddCustomDomainDialog from 'dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/AddCustomDomainDialog.vue';
|
||||||
import DNSConfigurationDialog from 'dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/DNSConfigurationDialog.vue';
|
import DNSConfigurationDialog from 'dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/DNSConfigurationDialog.vue';
|
||||||
@@ -11,11 +12,52 @@ const props = defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
isFetchingStatus: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['updatePortalConfiguration']);
|
const emit = defineEmits([
|
||||||
|
'updatePortalConfiguration',
|
||||||
|
'refreshStatus',
|
||||||
|
'sendCnameInstructions',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const SSL_STATUS = {
|
||||||
|
LIVE: ['active', 'staging_active'],
|
||||||
|
PENDING: [
|
||||||
|
'provisioned',
|
||||||
|
'pending',
|
||||||
|
'initializing',
|
||||||
|
'pending_validation',
|
||||||
|
'pending_deployment',
|
||||||
|
'pending_issuance',
|
||||||
|
'holding_deployment',
|
||||||
|
'holding_validation',
|
||||||
|
'pending_expiration',
|
||||||
|
'pending_cleanup',
|
||||||
|
'pending_deletion',
|
||||||
|
'staging_deployment',
|
||||||
|
'backup_issued',
|
||||||
|
],
|
||||||
|
ERROR: [
|
||||||
|
'blocked',
|
||||||
|
'inactive',
|
||||||
|
'moved',
|
||||||
|
'expired',
|
||||||
|
'deleted',
|
||||||
|
'timed_out_initializing',
|
||||||
|
'timed_out_validation',
|
||||||
|
'timed_out_issuance',
|
||||||
|
'timed_out_deployment',
|
||||||
|
'timed_out_deletion',
|
||||||
|
'deactivating',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const { isOnChatwootCloud } = useAccount();
|
||||||
|
|
||||||
const addCustomDomainDialogRef = ref(null);
|
const addCustomDomainDialogRef = ref(null);
|
||||||
const dnsConfigurationDialogRef = ref(null);
|
const dnsConfigurationDialogRef = ref(null);
|
||||||
@@ -25,6 +67,45 @@ const customDomainAddress = computed(
|
|||||||
() => props.activePortal?.custom_domain || ''
|
() => props.activePortal?.custom_domain || ''
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const sslSettings = computed(() => props.activePortal?.ssl_settings || {});
|
||||||
|
const verificationErrors = computed(
|
||||||
|
() => sslSettings.value.verification_errors || ''
|
||||||
|
);
|
||||||
|
|
||||||
|
const isLive = computed(() =>
|
||||||
|
SSL_STATUS.LIVE.includes(sslSettings.value.status)
|
||||||
|
);
|
||||||
|
const isPending = computed(() =>
|
||||||
|
SSL_STATUS.PENDING.includes(sslSettings.value.status)
|
||||||
|
);
|
||||||
|
const isError = computed(() =>
|
||||||
|
SSL_STATUS.ERROR.includes(sslSettings.value.status)
|
||||||
|
);
|
||||||
|
|
||||||
|
const statusText = computed(() => {
|
||||||
|
if (isLive.value)
|
||||||
|
return t(
|
||||||
|
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.STATUS.LIVE'
|
||||||
|
);
|
||||||
|
if (isPending.value)
|
||||||
|
return t(
|
||||||
|
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.STATUS.PENDING'
|
||||||
|
);
|
||||||
|
if (isError.value)
|
||||||
|
return t(
|
||||||
|
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.STATUS.ERROR'
|
||||||
|
);
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const statusColors = computed(() => {
|
||||||
|
if (isLive.value)
|
||||||
|
return { text: 'text-n-teal-11', bubble: 'outline-n-teal-6 bg-n-teal-9' };
|
||||||
|
if (isError.value)
|
||||||
|
return { text: 'text-n-ruby-11', bubble: 'outline-n-ruby-6 bg-n-ruby-9' };
|
||||||
|
return { text: 'text-n-amber-11', bubble: 'outline-n-amber-6 bg-n-amber-9' };
|
||||||
|
});
|
||||||
|
|
||||||
const updatePortalConfiguration = customDomain => {
|
const updatePortalConfiguration = customDomain => {
|
||||||
const portal = {
|
const portal = {
|
||||||
id: props.activePortal?.id,
|
id: props.activePortal?.id,
|
||||||
@@ -42,6 +123,17 @@ const closeDNSConfigurationDialog = () => {
|
|||||||
updatedDomainAddress.value = '';
|
updatedDomainAddress.value = '';
|
||||||
dnsConfigurationDialogRef.value.dialogRef.close();
|
dnsConfigurationDialogRef.value.dialogRef.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onClickRefreshSSLStatus = () => {
|
||||||
|
emit('refreshStatus');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickSend = email => {
|
||||||
|
emit('sendCnameInstructions', {
|
||||||
|
portalSlug: props.activePortal?.slug,
|
||||||
|
email,
|
||||||
|
});
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -63,33 +155,76 @@ const closeDNSConfigurationDialog = () => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col w-full gap-4">
|
<div class="flex flex-col w-full gap-4">
|
||||||
<div class="flex justify-between w-full gap-2">
|
<div class="flex items-center justify-between w-full gap-2">
|
||||||
<div
|
<div v-if="customDomainAddress" class="flex flex-col gap-1">
|
||||||
v-if="customDomainAddress"
|
<div class="flex items-center w-full h-8 gap-4">
|
||||||
class="flex items-center w-full h-8 gap-4"
|
<label class="text-sm font-medium text-n-slate-12">
|
||||||
>
|
{{
|
||||||
<label class="text-sm font-medium text-n-slate-12">
|
t(
|
||||||
|
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.LABEL'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</label>
|
||||||
|
<span class="text-sm text-n-slate-12">
|
||||||
|
{{ customDomainAddress }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
v-if="!isLive && isOnChatwootCloud"
|
||||||
|
class="text-sm text-n-slate-11"
|
||||||
|
>
|
||||||
{{
|
{{
|
||||||
t(
|
t(
|
||||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.LABEL'
|
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.STATUS_DESCRIPTION'
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</label>
|
|
||||||
<span class="text-sm text-n-slate-12">
|
|
||||||
{{ customDomainAddress }}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-end w-full">
|
<div class="flex items-center">
|
||||||
<Button
|
<div v-if="customDomainAddress" class="flex items-center gap-3">
|
||||||
v-if="customDomainAddress"
|
<div
|
||||||
color="slate"
|
v-if="statusText && isOnChatwootCloud"
|
||||||
:label="
|
v-tooltip="verificationErrors"
|
||||||
t(
|
class="flex items-center gap-3 flex-shrink-0"
|
||||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.EDIT_BUTTON'
|
>
|
||||||
)
|
<span
|
||||||
"
|
class="size-1.5 rounded-full outline outline-2 block flex-shrink-0"
|
||||||
@click="addCustomDomainDialogRef.dialogRef.open()"
|
:class="statusColors.bubble"
|
||||||
/>
|
/>
|
||||||
|
<span
|
||||||
|
:class="statusColors.text"
|
||||||
|
class="text-sm leading-[16px] font-medium"
|
||||||
|
>
|
||||||
|
{{ statusText }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="statusText && isOnChatwootCloud"
|
||||||
|
class="w-px h-3 bg-n-weak"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
slate
|
||||||
|
sm
|
||||||
|
link
|
||||||
|
:label="
|
||||||
|
t(
|
||||||
|
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.EDIT_BUTTON'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
class="hover:!no-underline flex-shrink-0"
|
||||||
|
@click="addCustomDomainDialogRef.dialogRef.open()"
|
||||||
|
/>
|
||||||
|
<div v-if="isOnChatwootCloud" class="w-px h-3 bg-n-weak" />
|
||||||
|
<Button
|
||||||
|
v-if="isOnChatwootCloud"
|
||||||
|
slate
|
||||||
|
sm
|
||||||
|
link
|
||||||
|
icon="i-lucide-refresh-ccw"
|
||||||
|
:class="isFetchingStatus && 'animate-spin'"
|
||||||
|
@click="onClickRefreshSSLStatus"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
v-else
|
v-else
|
||||||
:label="
|
:label="
|
||||||
@@ -112,7 +247,8 @@ const closeDNSConfigurationDialog = () => {
|
|||||||
<DNSConfigurationDialog
|
<DNSConfigurationDialog
|
||||||
ref="dnsConfigurationDialogRef"
|
ref="dnsConfigurationDialogRef"
|
||||||
:custom-domain="updatedDomainAddress || customDomainAddress"
|
:custom-domain="updatedDomainAddress || customDomainAddress"
|
||||||
@confirm="closeDNSConfigurationDialog"
|
@close="closeDNSConfigurationDialog"
|
||||||
|
@send="onClickSend"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+14
@@ -26,6 +26,8 @@ const emit = defineEmits([
|
|||||||
'updatePortal',
|
'updatePortal',
|
||||||
'updatePortalConfiguration',
|
'updatePortalConfiguration',
|
||||||
'deletePortal',
|
'deletePortal',
|
||||||
|
'refreshStatus',
|
||||||
|
'sendCnameInstructions',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@@ -36,6 +38,7 @@ const confirmDeletePortalDialogRef = ref(null);
|
|||||||
const currentPortalSlug = computed(() => route.params.portalSlug);
|
const currentPortalSlug = computed(() => route.params.portalSlug);
|
||||||
|
|
||||||
const isSwitchingPortal = useMapGetter('portals/isSwitchingPortal');
|
const isSwitchingPortal = useMapGetter('portals/isSwitchingPortal');
|
||||||
|
const isFetchingSSLStatus = useMapGetter('portals/isFetchingSSLStatus');
|
||||||
|
|
||||||
const activePortal = computed(() => {
|
const activePortal = computed(() => {
|
||||||
return props.portals?.find(portal => portal.slug === currentPortalSlug.value);
|
return props.portals?.find(portal => portal.slug === currentPortalSlug.value);
|
||||||
@@ -53,6 +56,14 @@ const handleUpdatePortalConfiguration = portal => {
|
|||||||
emit('updatePortalConfiguration', portal);
|
emit('updatePortalConfiguration', portal);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchSSLStatus = () => {
|
||||||
|
emit('refreshStatus');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendCnameInstructions = payload => {
|
||||||
|
emit('sendCnameInstructions', payload);
|
||||||
|
};
|
||||||
|
|
||||||
const openConfirmDeletePortalDialog = () => {
|
const openConfirmDeletePortalDialog = () => {
|
||||||
confirmDeletePortalDialogRef.value.dialogRef.open();
|
confirmDeletePortalDialogRef.value.dialogRef.open();
|
||||||
};
|
};
|
||||||
@@ -85,7 +96,10 @@ const handleDeletePortal = () => {
|
|||||||
<PortalConfigurationSettings
|
<PortalConfigurationSettings
|
||||||
:active-portal="activePortal"
|
:active-portal="activePortal"
|
||||||
:is-fetching="isFetching"
|
:is-fetching="isFetching"
|
||||||
|
:is-fetching-status="isFetchingSSLStatus"
|
||||||
@update-portal-configuration="handleUpdatePortalConfiguration"
|
@update-portal-configuration="handleUpdatePortalConfiguration"
|
||||||
|
@refresh-status="fetchSSLStatus"
|
||||||
|
@send-cname-instructions="handleSendCnameInstructions"
|
||||||
/>
|
/>
|
||||||
<div class="w-full h-px bg-n-weak" />
|
<div class="w-full h-px bg-n-weak" />
|
||||||
<div class="flex items-end justify-between w-full gap-4">
|
<div class="flex items-end justify-between w-full gap-4">
|
||||||
|
|||||||
@@ -63,11 +63,12 @@ const lastActivityAt = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const menuItems = computed(() => [
|
const menuItems = computed(() => [
|
||||||
{ key: 'delete', label: t('INBOX.MENU_ITEM.DELETE') },
|
|
||||||
{
|
{
|
||||||
key: isUnread.value ? 'mark_as_read' : 'mark_as_unread',
|
key: isUnread.value ? 'mark_as_read' : 'mark_as_unread',
|
||||||
|
icon: isUnread.value ? 'mail' : 'mail-unread',
|
||||||
label: t(`INBOX.MENU_ITEM.MARK_AS_${isUnread.value ? 'READ' : 'UNREAD'}`),
|
label: t(`INBOX.MENU_ITEM.MARK_AS_${isUnread.value ? 'READ' : 'UNREAD'}`),
|
||||||
},
|
},
|
||||||
|
{ key: 'delete', icon: 'delete', label: t('INBOX.MENU_ITEM.DELETE') },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const messageClasses = computed(() => ({
|
const messageClasses = computed(() => ({
|
||||||
@@ -153,7 +154,7 @@ onBeforeMount(contextMenuActions.close);
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
class="flex flex-col w-full gap-2 p-3 transition-all duration-300 ease-in-out cursor-pointer"
|
class="flex flex-col w-full gap-1 p-3 transition-all duration-300 ease-in-out cursor-pointer"
|
||||||
@contextmenu="contextMenuActions.open($event)"
|
@contextmenu="contextMenuActions.open($event)"
|
||||||
@click="emit('click')"
|
@click="emit('click')"
|
||||||
>
|
>
|
||||||
@@ -232,7 +233,7 @@ onBeforeMount(contextMenuActions.close);
|
|||||||
class="flex-shrink-0 text-n-slate-11 size-2.5"
|
class="flex-shrink-0 text-n-slate-11 size-2.5"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-sm text-n-slate-10">
|
<span class="text-xs text-n-slate-10">
|
||||||
{{ lastActivityAt }}
|
{{ lastActivityAt }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -52,9 +52,9 @@ const handleBreadcrumbClick = item => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section
|
<section
|
||||||
class="my-4 px-10 flex flex-col w-full h-screen overflow-y-auto bg-n-background"
|
class="mt-4 px-10 flex flex-col w-full h-screen overflow-y-auto bg-n-background"
|
||||||
>
|
>
|
||||||
<div class="max-w-[60rem] mx-auto flex flex-col w-full h-full">
|
<div class="max-w-[60rem] mx-auto flex flex-col w-full h-full mb-4">
|
||||||
<header class="mb-7 sticky top-0 z-10 bg-n-background">
|
<header class="mb-7 sticky top-0 z-10 bg-n-background">
|
||||||
<Breadcrumb :items="breadcrumbItems" @click="handleBreadcrumbClick" />
|
<Breadcrumb :items="breadcrumbItems" @click="handleBreadcrumbClick" />
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
<script setup>
|
||||||
|
import AddNewRulesDialog from './AddNewRulesDialog.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Story
|
||||||
|
title="Captain/Assistant/AddNewRulesDialog"
|
||||||
|
:layout="{ type: 'grid', width: '800px' }"
|
||||||
|
>
|
||||||
|
<Variant title="Default">
|
||||||
|
<div class="px-4 py-4 bg-n-background h-[200px]">
|
||||||
|
<AddNewRulesDialog
|
||||||
|
button-label="Add a guardrail"
|
||||||
|
placeholder="Type in another guardrail..."
|
||||||
|
confirm-label="Create"
|
||||||
|
cancel-label="Cancel"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
</Story>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useToggle } from '@vueuse/core';
|
||||||
|
import { vOnClickOutside } from '@vueuse/components';
|
||||||
|
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import InlineInput from 'dashboard/components-next/inline-input/InlineInput.vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
buttonLabel: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
confirmLabel: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
cancelLabel: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['add']);
|
||||||
|
|
||||||
|
const modelValue = defineModel({
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [showPopover, togglePopover] = useToggle();
|
||||||
|
const onClickAdd = () => {
|
||||||
|
if (!modelValue.value?.trim()) return;
|
||||||
|
emit('add', modelValue.value.trim());
|
||||||
|
modelValue.value = '';
|
||||||
|
togglePopover(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickCancel = () => {
|
||||||
|
togglePopover(false);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-on-click-outside="() => togglePopover(false)"
|
||||||
|
class="inline-flex relative"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
:label="buttonLabel"
|
||||||
|
sm
|
||||||
|
slate
|
||||||
|
class="flex-shrink-0"
|
||||||
|
@click="togglePopover(!showPopover)"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="showPopover"
|
||||||
|
class="absolute w-[26.5rem] top-9 z-50 ltr:left-0 rtl:right-0 flex flex-col gap-5 bg-n-alpha-3 backdrop-blur-[100px] p-4 rounded-xl border border-n-weak shadow-md"
|
||||||
|
>
|
||||||
|
<InlineInput
|
||||||
|
v-model="modelValue"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
@keyup.enter="onClickAdd"
|
||||||
|
/>
|
||||||
|
<div class="flex gap-2 justify-between">
|
||||||
|
<Button
|
||||||
|
:label="cancelLabel"
|
||||||
|
sm
|
||||||
|
link
|
||||||
|
slate
|
||||||
|
class="h-10 hover:!no-underline"
|
||||||
|
@click="onClickCancel"
|
||||||
|
/>
|
||||||
|
<Button :label="confirmLabel" sm @click="onClickAdd" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script setup>
|
||||||
|
import AddNewRulesInput from './AddNewRulesInput.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Story
|
||||||
|
title="Captain/Assistant/AddNewRulesInput"
|
||||||
|
:layout="{ type: 'grid', width: '800px' }"
|
||||||
|
>
|
||||||
|
<Variant title="Default">
|
||||||
|
<div class="px-6 py-4 bg-n-background">
|
||||||
|
<AddNewRulesInput
|
||||||
|
placeholder="Type in another response guideline..."
|
||||||
|
label="Add and save (↵)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
</Story>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<script setup>
|
||||||
|
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import InlineInput from 'dashboard/components-next/inline-input/InlineInput.vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['add']);
|
||||||
|
|
||||||
|
const modelValue = defineModel({
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const onClickAdd = () => {
|
||||||
|
if (!modelValue.value?.trim()) return;
|
||||||
|
emit('add', modelValue.value.trim());
|
||||||
|
modelValue.value = '';
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex py-3 ltr:pl-3 h-16 rtl:pr-3 ltr:pr-4 rtl:pl-4 items-center gap-3 rounded-xl bg-n-solid-2 outline-1 outline outline-n-container"
|
||||||
|
>
|
||||||
|
<Icon icon="i-lucide-plus" class="text-n-slate-10 size-5 flex-shrink-0" />
|
||||||
|
|
||||||
|
<InlineInput
|
||||||
|
v-model="modelValue"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
@keyup.enter="onClickAdd"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
:label="label"
|
||||||
|
ghost
|
||||||
|
xs
|
||||||
|
slate
|
||||||
|
class="!text-sm !text-n-slate-11 flex-shrink-0"
|
||||||
|
@click="onClickAdd"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, reactive } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useToggle } from '@vueuse/core';
|
||||||
|
import { useVuelidate } from '@vuelidate/core';
|
||||||
|
import { vOnClickOutside } from '@vueuse/components';
|
||||||
|
import { required, minLength } from '@vuelidate/validators';
|
||||||
|
|
||||||
|
import Input from 'dashboard/components-next/input/Input.vue';
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import TextArea from 'dashboard/components-next/textarea/TextArea.vue';
|
||||||
|
import Editor from 'dashboard/components-next/Editor/Editor.vue';
|
||||||
|
|
||||||
|
const emit = defineEmits(['add']);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const [showPopover, togglePopover] = useToggle();
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
id: '',
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
instruction: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
title: { required, minLength: minLength(1) },
|
||||||
|
description: { required },
|
||||||
|
instruction: { required },
|
||||||
|
};
|
||||||
|
|
||||||
|
const v$ = useVuelidate(rules, state);
|
||||||
|
|
||||||
|
const titleError = computed(() =>
|
||||||
|
v$.value.title.$error
|
||||||
|
? t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.TITLE.ERROR')
|
||||||
|
: ''
|
||||||
|
);
|
||||||
|
|
||||||
|
const descriptionError = computed(() =>
|
||||||
|
v$.value.description.$error
|
||||||
|
? t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.DESCRIPTION.ERROR')
|
||||||
|
: ''
|
||||||
|
);
|
||||||
|
|
||||||
|
const instructionError = computed(() =>
|
||||||
|
v$.value.instruction.$error
|
||||||
|
? t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.INSTRUCTION.ERROR')
|
||||||
|
: ''
|
||||||
|
);
|
||||||
|
|
||||||
|
const resetState = () => {
|
||||||
|
Object.assign(state, {
|
||||||
|
id: '',
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
instruction: '',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickAdd = async () => {
|
||||||
|
v$.value.$touch();
|
||||||
|
if (v$.value.$invalid) return;
|
||||||
|
|
||||||
|
await emit('add', state);
|
||||||
|
resetState();
|
||||||
|
togglePopover(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickCancel = () => {
|
||||||
|
togglePopover(false);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-on-click-outside="() => togglePopover(false)"
|
||||||
|
class="inline-flex relative"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
:label="t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.CREATE')"
|
||||||
|
sm
|
||||||
|
slate
|
||||||
|
class="flex-shrink-0"
|
||||||
|
@click="togglePopover(!showPopover)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="showPopover"
|
||||||
|
class="w-[31.25rem] absolute top-10 ltr:left-0 rtl:right-0 bg-n-alpha-3 backdrop-blur-[100px] p-6 rounded-xl border border-n-weak shadow-md flex flex-col gap-6 z-50"
|
||||||
|
>
|
||||||
|
<h3 class="text-base font-medium text-n-slate-12">
|
||||||
|
{{ t(`CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.TITLE`) }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<Input
|
||||||
|
v-model="state.title"
|
||||||
|
:label="t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.TITLE.LABEL')"
|
||||||
|
:placeholder="
|
||||||
|
t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.TITLE.PLACEHOLDER')
|
||||||
|
"
|
||||||
|
:message="titleError"
|
||||||
|
:message-type="titleError ? 'error' : 'info'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextArea
|
||||||
|
v-model="state.description"
|
||||||
|
:label="
|
||||||
|
t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.DESCRIPTION.LABEL')
|
||||||
|
"
|
||||||
|
:placeholder="
|
||||||
|
t(
|
||||||
|
'CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.DESCRIPTION.PLACEHOLDER'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:message="descriptionError"
|
||||||
|
:message-type="descriptionError ? 'error' : 'info'"
|
||||||
|
show-character-count
|
||||||
|
/>
|
||||||
|
<Editor
|
||||||
|
v-model="state.instruction"
|
||||||
|
:label="
|
||||||
|
t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.INSTRUCTION.LABEL')
|
||||||
|
"
|
||||||
|
:placeholder="
|
||||||
|
t(
|
||||||
|
'CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.INSTRUCTION.PLACEHOLDER'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:message="instructionError"
|
||||||
|
:message-type="instructionError ? 'error' : 'info'"
|
||||||
|
:show-character-count="false"
|
||||||
|
enable-captain-tools
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between w-full gap-3">
|
||||||
|
<Button
|
||||||
|
variant="faded"
|
||||||
|
color="slate"
|
||||||
|
:label="t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.CANCEL')"
|
||||||
|
class="w-full bg-n-alpha-2 !text-n-blue-text hover:bg-n-alpha-3"
|
||||||
|
@click="onClickCancel"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
:label="t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.CREATE')"
|
||||||
|
class="w-full"
|
||||||
|
@click="onClickAdd"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue';
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
allItems: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
selectAllLabel: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
selectedCountLabel: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
deleteLabel: {
|
||||||
|
type: String,
|
||||||
|
default: 'Delete',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['bulkDelete']);
|
||||||
|
|
||||||
|
const modelValue = defineModel({
|
||||||
|
type: Set,
|
||||||
|
default: () => new Set(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedCount = computed(() => modelValue.value.size);
|
||||||
|
const totalCount = computed(() => props.allItems.length);
|
||||||
|
|
||||||
|
const hasSelected = computed(() => selectedCount.value > 0);
|
||||||
|
const isIndeterminate = computed(
|
||||||
|
() => hasSelected.value && selectedCount.value < totalCount.value
|
||||||
|
);
|
||||||
|
const allSelected = computed(
|
||||||
|
() => totalCount.value > 0 && selectedCount.value === totalCount.value
|
||||||
|
);
|
||||||
|
|
||||||
|
const bulkCheckboxState = computed({
|
||||||
|
get: () => allSelected.value,
|
||||||
|
set: shouldSelectAll => {
|
||||||
|
const newSelectedIds = shouldSelectAll
|
||||||
|
? new Set(props.allItems.map(item => item.id))
|
||||||
|
: new Set();
|
||||||
|
modelValue.value = newSelectedIds;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<transition
|
||||||
|
name="slide-fade"
|
||||||
|
enter-active-class="transition-all duration-300 ease-out"
|
||||||
|
enter-from-class="opacity-0 transform ltr:-translate-x-4 rtl:translate-x-4"
|
||||||
|
enter-to-class="opacity-100 transform translate-x-0"
|
||||||
|
leave-active-class="hidden opacity-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="hasSelected"
|
||||||
|
class="flex items-center gap-3 py-1 ltr:pl-3 rtl:pr-3 ltr:pr-4 rtl:pl-4 rounded-lg bg-n-solid-2 outline outline-1 outline-n-container shadow"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<Checkbox
|
||||||
|
v-model="bulkCheckboxState"
|
||||||
|
:indeterminate="isIndeterminate"
|
||||||
|
/>
|
||||||
|
<span class="text-sm font-medium text-n-slate-12 tabular-nums">
|
||||||
|
{{ selectAllLabel }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-sm text-n-slate-10 tabular-nums">
|
||||||
|
{{ selectedCountLabel }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="h-4 w-px bg-n-strong" />
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<slot name="actions" :selected-count="selectedCount">
|
||||||
|
<Button
|
||||||
|
:label="deleteLabel"
|
||||||
|
sm
|
||||||
|
ruby
|
||||||
|
ghost
|
||||||
|
class="!px-1.5"
|
||||||
|
icon="i-lucide-trash"
|
||||||
|
@click="emit('bulkDelete')"
|
||||||
|
/>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex items-center gap-3">
|
||||||
|
<slot name="default-actions" />
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<script setup>
|
||||||
|
import RuleCard from './RuleCard.vue';
|
||||||
|
|
||||||
|
const sampleRules = [
|
||||||
|
{ id: 1, content: 'Block sensitive personal information', selectable: true },
|
||||||
|
{ id: 2, content: 'Reject offensive language', selectable: true },
|
||||||
|
{ id: 3, content: 'Deflect legal or medical advice', selectable: true },
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Story
|
||||||
|
title="Captain/Assistant/RuleCard"
|
||||||
|
:layout="{ type: 'grid', width: '800px' }"
|
||||||
|
>
|
||||||
|
<Variant title="Selectable List">
|
||||||
|
<div class="flex flex-col gap-4 px-20 py-4 bg-n-background">
|
||||||
|
<RuleCard
|
||||||
|
v-for="rule in sampleRules"
|
||||||
|
:id="rule.id"
|
||||||
|
:key="rule.id"
|
||||||
|
:content="rule.content"
|
||||||
|
:selectable="rule.selectable"
|
||||||
|
@select="id => console.log('Selected rule', id)"
|
||||||
|
@edit="id => console.log('Edit', id)"
|
||||||
|
@delete="id => console.log('Delete', id)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
|
||||||
|
<Variant title="Non-Selectable">
|
||||||
|
<div class="flex flex-col gap-4 px-20 py-4 bg-n-background">
|
||||||
|
<RuleCard id="4" content="Replies should be friendly and clear." />
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
</Story>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import CardLayout from 'dashboard/components-next/CardLayout.vue';
|
||||||
|
import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue';
|
||||||
|
import InlineInput from 'dashboard/components-next/inline-input/InlineInput.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
id: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
selectable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
isSelected: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['select', 'hover', 'edit', 'delete']);
|
||||||
|
|
||||||
|
const modelValue = computed({
|
||||||
|
get: () => props.isSelected,
|
||||||
|
set: () => emit('select', props.id),
|
||||||
|
});
|
||||||
|
|
||||||
|
const isEditing = ref(false);
|
||||||
|
const editedContent = ref(props.content);
|
||||||
|
|
||||||
|
// Local content to display to avoid flicker until parent prop updates on inline edit
|
||||||
|
const localContent = ref(props.content);
|
||||||
|
|
||||||
|
// Keeps localContent in sync when parent updates content prop
|
||||||
|
watch(
|
||||||
|
() => props.content,
|
||||||
|
newVal => {
|
||||||
|
localContent.value = newVal;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const startEdit = () => {
|
||||||
|
isEditing.value = true;
|
||||||
|
editedContent.value = props.content;
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveEdit = () => {
|
||||||
|
isEditing.value = false;
|
||||||
|
// Update local content
|
||||||
|
localContent.value = editedContent.value;
|
||||||
|
emit('edit', { id: props.id, content: editedContent.value });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CardLayout
|
||||||
|
selectable
|
||||||
|
class="relative [&>div]:!py-5 [&>div]:ltr:!pr-4 [&>div]:rtl:!pl-4"
|
||||||
|
layout="row"
|
||||||
|
@mouseenter="emit('hover', true)"
|
||||||
|
@mouseleave="emit('hover', false)"
|
||||||
|
>
|
||||||
|
<div v-show="selectable" class="absolute top-6 ltr:left-3 rtl:right-3">
|
||||||
|
<Checkbox v-model="modelValue" />
|
||||||
|
</div>
|
||||||
|
<InlineInput
|
||||||
|
v-if="isEditing"
|
||||||
|
v-model="editedContent"
|
||||||
|
focus-on-mount
|
||||||
|
@keyup.enter="saveEdit"
|
||||||
|
/>
|
||||||
|
<span v-else class="flex items-center gap-2 text-sm text-n-slate-12">
|
||||||
|
{{ localContent }}
|
||||||
|
</span>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Button icon="i-lucide-pen" slate xs ghost @click="startEdit" />
|
||||||
|
<span class="w-px h-4 bg-n-weak" />
|
||||||
|
<Button
|
||||||
|
icon="i-lucide-trash"
|
||||||
|
slate
|
||||||
|
xs
|
||||||
|
ghost
|
||||||
|
@click="emit('delete', id)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardLayout>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<script setup>
|
||||||
|
import ScenariosCard from './ScenariosCard.vue';
|
||||||
|
|
||||||
|
const sampleScenarios = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Refund Order',
|
||||||
|
description: 'User requests a refund for a recent purchase.',
|
||||||
|
instruction:
|
||||||
|
'Gather order details and reason for refund. Use [Order Search](tool://order_search) then submit with [Refund Payment](tool://refund_payment).',
|
||||||
|
tools: ['order_search', 'refund_payment'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Bug Report',
|
||||||
|
description: 'Customer reports a bug in the mobile app.',
|
||||||
|
instruction:
|
||||||
|
'Ask for reproduction steps and environment. Check [Known Issues](tool://known_issues) then create ticket with [Create Bug Report](tool://bug_report_create).',
|
||||||
|
tools: ['known_issues', 'bug_report_create'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Story
|
||||||
|
title="Captain/Assistant/ScenariosCard"
|
||||||
|
:layout="{ type: 'grid', width: '800px' }"
|
||||||
|
>
|
||||||
|
<Variant title="Default">
|
||||||
|
<div
|
||||||
|
v-for="scenario in sampleScenarios"
|
||||||
|
:key="scenario.id"
|
||||||
|
class="px-4 py-4 bg-n-background"
|
||||||
|
>
|
||||||
|
<ScenariosCard
|
||||||
|
:id="scenario.id"
|
||||||
|
:title="scenario.title"
|
||||||
|
:description="scenario.description"
|
||||||
|
:instruction="scenario.instruction"
|
||||||
|
:tools="scenario.tools"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
</Story>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, h, reactive } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useToggle } from '@vueuse/core';
|
||||||
|
import { useVuelidate } from '@vuelidate/core';
|
||||||
|
import { required, minLength } from '@vuelidate/validators';
|
||||||
|
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||||
|
import Input from 'dashboard/components-next/input/Input.vue';
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import TextArea from 'dashboard/components-next/textarea/TextArea.vue';
|
||||||
|
import Editor from 'dashboard/components-next/Editor/Editor.vue';
|
||||||
|
import CardLayout from 'dashboard/components-next/CardLayout.vue';
|
||||||
|
import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
id: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
instruction: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
selectable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
isSelected: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['select', 'hover', 'delete', 'update']);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { formatMessage } = useMessageFormatter();
|
||||||
|
|
||||||
|
const modelValue = computed({
|
||||||
|
get: () => props.isSelected,
|
||||||
|
set: () => emit('select', props.id),
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
id: '',
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
instruction: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [isEditing, toggleEditing] = useToggle();
|
||||||
|
|
||||||
|
const startEdit = () => {
|
||||||
|
Object.assign(state, {
|
||||||
|
id: props.id,
|
||||||
|
title: props.title,
|
||||||
|
description: props.description,
|
||||||
|
instruction: props.instruction,
|
||||||
|
tools: props.tools,
|
||||||
|
});
|
||||||
|
toggleEditing(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
title: { required, minLength: minLength(1) },
|
||||||
|
description: { required },
|
||||||
|
instruction: { required },
|
||||||
|
};
|
||||||
|
|
||||||
|
const v$ = useVuelidate(rules, state);
|
||||||
|
|
||||||
|
const titleError = computed(() =>
|
||||||
|
v$.value.title.$error
|
||||||
|
? t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.TITLE.ERROR')
|
||||||
|
: ''
|
||||||
|
);
|
||||||
|
|
||||||
|
const descriptionError = computed(() =>
|
||||||
|
v$.value.description.$error
|
||||||
|
? t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.DESCRIPTION.ERROR')
|
||||||
|
: ''
|
||||||
|
);
|
||||||
|
|
||||||
|
const onClickUpdate = () => {
|
||||||
|
v$.value.$touch();
|
||||||
|
if (v$.value.$invalid) return;
|
||||||
|
emit('update', { ...state });
|
||||||
|
toggleEditing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const instructionError = computed(() =>
|
||||||
|
v$.value.instruction.$error
|
||||||
|
? t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.INSTRUCTION.ERROR')
|
||||||
|
: ''
|
||||||
|
);
|
||||||
|
|
||||||
|
const LINK_INSTRUCTION_CLASS =
|
||||||
|
'[&_a[href^="tool://"]]:text-n-iris-11 [&_a:not([href^="tool://"])]:text-n-slate-12 [&_a]:pointer-events-none [&_a]:cursor-default';
|
||||||
|
|
||||||
|
const renderInstruction = instruction => () =>
|
||||||
|
h('p', {
|
||||||
|
class: `text-sm text-n-slate-12 py-4 mb-0 [&_ol]:list-decimal ${LINK_INSTRUCTION_CLASS}`,
|
||||||
|
innerHTML: instruction,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CardLayout
|
||||||
|
selectable
|
||||||
|
class="relative [&>div]:!py-4"
|
||||||
|
:class="{
|
||||||
|
'[&>div]:ltr:!pr-4 [&>div]:rtl:!pl-4': !isEditing,
|
||||||
|
'[&>div]:ltr:!pr-10 [&>div]:rtl:!pl-10': isEditing,
|
||||||
|
}"
|
||||||
|
layout="row"
|
||||||
|
@mouseenter="emit('hover', true)"
|
||||||
|
@mouseleave="emit('hover', false)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-show="selectable && !isEditing"
|
||||||
|
class="absolute top-[1.125rem] ltr:left-3 rtl:right-3"
|
||||||
|
>
|
||||||
|
<Checkbox v-model="modelValue" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!isEditing" class="flex flex-col w-full">
|
||||||
|
<div class="flex items-start justify-between w-full gap-2">
|
||||||
|
<div class="flex flex-col items-start">
|
||||||
|
<span class="text-sm text-n-slate-12 font-medium">{{ title }}</span>
|
||||||
|
<span class="text-sm text-n-slate-11 mt-2">
|
||||||
|
{{ description }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<!-- <Button label="Test" slate xs ghost class="!text-sm" />
|
||||||
|
<span class="w-px h-4 bg-n-weak" /> -->
|
||||||
|
<Button icon="i-lucide-pen" slate xs ghost @click="startEdit" />
|
||||||
|
<span class="w-px h-4 bg-n-weak" />
|
||||||
|
<Button
|
||||||
|
icon="i-lucide-trash"
|
||||||
|
slate
|
||||||
|
xs
|
||||||
|
ghost
|
||||||
|
@click="emit('delete', id)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<component :is="renderInstruction(formatMessage(instruction, false))" />
|
||||||
|
<span class="text-sm text-n-slate-11 font-medium mb-1">
|
||||||
|
{{ t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.SUGGESTED.TOOLS_USED') }}
|
||||||
|
{{ tools?.map(tool => `@${tool}`).join(', ') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-else class="overflow-hidden flex flex-col gap-4 w-full">
|
||||||
|
<Input
|
||||||
|
v-model="state.title"
|
||||||
|
:label="t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.TITLE.LABEL')"
|
||||||
|
:placeholder="
|
||||||
|
t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.TITLE.PLACEHOLDER')
|
||||||
|
"
|
||||||
|
:message="titleError"
|
||||||
|
:message-type="titleError ? 'error' : 'info'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextArea
|
||||||
|
v-model="state.description"
|
||||||
|
:label="
|
||||||
|
t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.DESCRIPTION.LABEL')
|
||||||
|
"
|
||||||
|
:placeholder="
|
||||||
|
t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.DESCRIPTION.PLACEHOLDER')
|
||||||
|
"
|
||||||
|
:message="descriptionError"
|
||||||
|
:message-type="descriptionError ? 'error' : 'info'"
|
||||||
|
show-character-count
|
||||||
|
/>
|
||||||
|
<Editor
|
||||||
|
v-model="state.instruction"
|
||||||
|
:label="
|
||||||
|
t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.INSTRUCTION.LABEL')
|
||||||
|
"
|
||||||
|
:placeholder="
|
||||||
|
t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.INSTRUCTION.PLACEHOLDER')
|
||||||
|
"
|
||||||
|
:message="instructionError"
|
||||||
|
:message-type="instructionError ? 'error' : 'info'"
|
||||||
|
:show-character-count="false"
|
||||||
|
enable-captain-tools
|
||||||
|
/>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<Button
|
||||||
|
faded
|
||||||
|
slate
|
||||||
|
sm
|
||||||
|
:label="t('CAPTAIN.ASSISTANTS.SCENARIOS.UPDATE.CANCEL')"
|
||||||
|
@click="toggleEditing(false)"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
sm
|
||||||
|
:label="t('CAPTAIN.ASSISTANTS.SCENARIOS.UPDATE.UPDATE')"
|
||||||
|
@click="onClickUpdate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardLayout>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<script setup>
|
||||||
|
import SuggestedRules from './SuggestedRules.vue';
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
|
||||||
|
const guidelinesExample = [
|
||||||
|
{
|
||||||
|
content:
|
||||||
|
'Block queries that share or request sensitive personal information (e.g. phone numbers, passwords).',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content:
|
||||||
|
'Reject queries that include offensive, discriminatory, or threatening language.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content:
|
||||||
|
'Deflect when the assistant is asked for legal or medical diagnosis or treatment.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Story
|
||||||
|
title="Captain/Assistant/SuggestedRules"
|
||||||
|
:layout="{ type: 'grid', width: '800px' }"
|
||||||
|
>
|
||||||
|
<Variant title="Suggested Rules List">
|
||||||
|
<div class="px-20 py-4 bg-n-background">
|
||||||
|
<SuggestedRules
|
||||||
|
title="Example response guidelines"
|
||||||
|
:items="guidelinesExample"
|
||||||
|
>
|
||||||
|
<template #default="{ item }">
|
||||||
|
<span class="text-sm text-n-slate-12">{{ item.content }}</span>
|
||||||
|
<Button
|
||||||
|
label="Add this"
|
||||||
|
ghost
|
||||||
|
xs
|
||||||
|
slate
|
||||||
|
class="!text-sm !text-n-slate-11 flex-shrink-0"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</SuggestedRules>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
</Story>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['add', 'close']);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const onAddClick = () => {
|
||||||
|
emit('add');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickClose = () => {
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex flex-col items-start self-stretch rounded-xl w-full overflow-hidden border border-dashed border-n-strong"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between w-full gap-3 px-4 pb-1 pt-4">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<h5 class="text-sm font-medium text-n-slate-11">{{ title }}</h5>
|
||||||
|
<span class="h-3 w-px bg-n-weak" />
|
||||||
|
<Button
|
||||||
|
:label="t('CAPTAIN.ASSISTANTS.GUARDRAILS.ADD.SUGGESTED.ADD')"
|
||||||
|
ghost
|
||||||
|
xs
|
||||||
|
slate
|
||||||
|
class="!text-sm !text-n-slate-11 flex-shrink-0"
|
||||||
|
@click="onAddClick"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
ghost
|
||||||
|
xs
|
||||||
|
slate
|
||||||
|
icon="i-lucide-x"
|
||||||
|
class="!text-sm !text-n-slate-11 flex-shrink-0"
|
||||||
|
@click="onClickClose"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex flex-col items-start divide-y divide-n-strong divide-dashed w-full"
|
||||||
|
>
|
||||||
|
<div v-for="item in items" :key="item.content" class="w-full px-4 py-4">
|
||||||
|
<slot :item="item" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import ToolsDropdown from './ToolsDropdown.vue';
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
id: 'order_search',
|
||||||
|
title: 'Order Search',
|
||||||
|
description: 'Lookup orders by customer ID, email, or order number',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'refund_payment',
|
||||||
|
title: 'Refund Payment',
|
||||||
|
description: 'Initiates a refund on a specific payment',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fetch_customer',
|
||||||
|
title: 'Fetch Customer',
|
||||||
|
description: 'Pulls customer details (email, tags, last seen, etc.)',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const selectedIndex = ref(0);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Story
|
||||||
|
title="Captain/Assistant/ToolsDropdown"
|
||||||
|
:layout="{ type: 'grid', width: '600px' }"
|
||||||
|
>
|
||||||
|
<Variant title="Default">
|
||||||
|
<div class="relative h-80 bg-n-background p-4">
|
||||||
|
<ToolsDropdown :items="items" :selected-index="selectedIndex" />
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
</Story>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, watch, nextTick } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
selectedIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['select']);
|
||||||
|
|
||||||
|
const toolsDropdownRef = ref(null);
|
||||||
|
|
||||||
|
const onItemClick = idx => emit('select', idx);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.selectedIndex,
|
||||||
|
() => {
|
||||||
|
nextTick(() => {
|
||||||
|
const el = toolsDropdownRef.value?.querySelector(
|
||||||
|
`#tool-item-${props.selectedIndex}`
|
||||||
|
);
|
||||||
|
if (el) {
|
||||||
|
el.scrollIntoView({ block: 'nearest', behavior: 'auto' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="toolsDropdownRef"
|
||||||
|
class="w-[22.5rem] p-2 flex flex-col gap-1 z-50 absolute rounded-xl bg-n-alpha-3 shadow outline outline-1 outline-n-weak backdrop-blur-[50px] max-h-[20rem] overflow-y-auto"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(tool, idx) in items"
|
||||||
|
:id="`tool-item-${idx}`"
|
||||||
|
:key="tool.id || idx"
|
||||||
|
:class="{ 'bg-n-alpha-black2': idx === selectedIndex }"
|
||||||
|
class="flex flex-col gap-1 rounded-md py-2 px-2 cursor-pointer hover:bg-n-alpha-black2"
|
||||||
|
@click="onItemClick(idx)"
|
||||||
|
>
|
||||||
|
<span class="text-n-slate-12 font-medium text-sm">{{ tool.title }}</span>
|
||||||
|
<span class="text-n-slate-11 text-sm">{{ tool.description }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -6,7 +6,7 @@ import DropdownBody from './base/DropdownBody.vue';
|
|||||||
import DropdownSection from './base/DropdownSection.vue';
|
import DropdownSection from './base/DropdownSection.vue';
|
||||||
import DropdownItem from './base/DropdownItem.vue';
|
import DropdownItem from './base/DropdownItem.vue';
|
||||||
import DropdownSeparator from './base/DropdownSeparator.vue';
|
import DropdownSeparator from './base/DropdownSeparator.vue';
|
||||||
import WootSwitch from 'components/ui/Switch.vue';
|
import ToggleSwitch from 'dashboard/components-next/switch/Switch.vue';
|
||||||
|
|
||||||
const currentUserAutoOffline = ref(false);
|
const currentUserAutoOffline = ref(false);
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ const menuItems = ref([
|
|||||||
<DropdownItem label="Contact Support" class="justify-between">
|
<DropdownItem label="Contact Support" class="justify-between">
|
||||||
<span>{{ $t('SIDEBAR.SET_AUTO_OFFLINE.TEXT') }}</span>
|
<span>{{ $t('SIDEBAR.SET_AUTO_OFFLINE.TEXT') }}</span>
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
<WootSwitch v-model="currentUserAutoOffline" />
|
<ToggleSwitch v-model="currentUserAutoOffline" />
|
||||||
</div>
|
</div>
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownSection>
|
</DropdownSection>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { computed, defineModel, h, watch, ref } from 'vue';
|
import { computed, defineModel, h, watch, ref } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import Button from 'next/button/Button.vue';
|
import Button from 'next/button/Button.vue';
|
||||||
|
import Input from 'dashboard/components-next/input/Input.vue';
|
||||||
import FilterSelect from './inputs/FilterSelect.vue';
|
import FilterSelect from './inputs/FilterSelect.vue';
|
||||||
import MultiSelect from './inputs/MultiSelect.vue';
|
import MultiSelect from './inputs/MultiSelect.vue';
|
||||||
import SingleSelect from './inputs/SingleSelect.vue';
|
import SingleSelect from './inputs/SingleSelect.vue';
|
||||||
@@ -178,11 +179,11 @@ defineExpose({ validate });
|
|||||||
disable-search
|
disable-search
|
||||||
:options="booleanOptions"
|
:options="booleanOptions"
|
||||||
/>
|
/>
|
||||||
<input
|
<Input
|
||||||
v-else
|
v-else
|
||||||
v-model="values"
|
v-model="values"
|
||||||
:type="inputType === 'date' ? 'date' : 'text'"
|
:type="inputType === 'date' ? 'date' : 'text'"
|
||||||
class="py-1.5 px-3 text-n-slate-12 bg-n-alpha-1 text-sm rounded-lg reset-base"
|
class="[&>input]:h-8 [&>input]:py-1.5 [&>input]:outline-offset-0"
|
||||||
:placeholder="t('FILTER.INPUT_PLACEHOLDER')"
|
:placeholder="t('FILTER.INPUT_PLACEHOLDER')"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useContactFilterContext } from './contactProvider.js';
|
|||||||
import { useSnakeCase } from 'dashboard/composables/useTransformKeys';
|
import { useSnakeCase } from 'dashboard/composables/useTransformKeys';
|
||||||
|
|
||||||
import Button from 'next/button/Button.vue';
|
import Button from 'next/button/Button.vue';
|
||||||
|
import Input from 'dashboard/components-next/input/Input.vue';
|
||||||
import ConditionRow from './ConditionRow.vue';
|
import ConditionRow from './ConditionRow.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -109,16 +110,13 @@ const outsideClickHandler = [
|
|||||||
{{ filterModalHeaderTitle }}
|
{{ filterModalHeaderTitle }}
|
||||||
</h3>
|
</h3>
|
||||||
<div v-if="props.isSegmentView">
|
<div v-if="props.isSegmentView">
|
||||||
<label class="pb-6 border-b border-n-weak">
|
<div class="pb-6 border-b border-n-weak">
|
||||||
<div class="mb-2 text-sm text-n-slate-11">
|
<Input
|
||||||
{{ $t('CONTACTS_LAYOUT.FILTER.SEGMENT.LABEL') }}
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
v-model="segmentNameLocal"
|
v-model="segmentNameLocal"
|
||||||
class="py-1.5 px-3 text-n-slate-12 bg-n-alpha-1 text-sm rounded-lg reset-base w-full"
|
:label="$t('CONTACTS_LAYOUT.FILTER.SEGMENT.LABEL')"
|
||||||
:placeholder="t('CONTACTS_LAYOUT.FILTER.SEGMENT.INPUT_PLACEHOLDER')"
|
:placeholder="t('CONTACTS_LAYOUT.FILTER.SEGMENT.INPUT_PLACEHOLDER')"
|
||||||
/>
|
/>
|
||||||
</label>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="grid gap-4 list-none">
|
<ul class="grid gap-4 list-none">
|
||||||
<template v-for="(filter, index) in filters" :key="filter.id">
|
<template v-for="(filter, index) in filters" :key="filter.id">
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useConversationFilterContext } from './provider.js';
|
|||||||
import { useSnakeCase } from 'dashboard/composables/useTransformKeys';
|
import { useSnakeCase } from 'dashboard/composables/useTransformKeys';
|
||||||
|
|
||||||
import Button from 'next/button/Button.vue';
|
import Button from 'next/button/Button.vue';
|
||||||
|
import Input from 'dashboard/components-next/input/Input.vue';
|
||||||
import ConditionRow from './ConditionRow.vue';
|
import ConditionRow from './ConditionRow.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -110,16 +111,13 @@ const outsideClickHandler = [
|
|||||||
{{ filterModalHeaderTitle }}
|
{{ filterModalHeaderTitle }}
|
||||||
</h3>
|
</h3>
|
||||||
<div v-if="props.isFolderView">
|
<div v-if="props.isFolderView">
|
||||||
<label class="border-b border-n-weak pb-6">
|
<div class="border-b border-n-weak pb-6">
|
||||||
<div class="text-n-slate-11 text-sm mb-2">
|
<Input
|
||||||
{{ t('FILTER.FOLDER_LABEL') }}
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
v-model="folderNameLocal"
|
v-model="folderNameLocal"
|
||||||
class="py-1.5 px-3 text-n-slate-12 bg-n-alpha-1 text-sm rounded-lg reset-base w-full"
|
:label="t('FILTER.FOLDER_LABEL')"
|
||||||
:placeholder="t('FILTER.INPUT_PLACEHOLDER')"
|
:placeholder="t('FILTER.INPUT_PLACEHOLDER')"
|
||||||
/>
|
/>
|
||||||
</label>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="grid gap-4 list-none">
|
<ul class="grid gap-4 list-none">
|
||||||
<template v-for="(filter, index) in filters" :key="filter.id">
|
<template v-for="(filter, index) in filters" :key="filter.id">
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import { CONTACTS_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
|||||||
import { vOnClickOutside } from '@vueuse/components';
|
import { vOnClickOutside } from '@vueuse/components';
|
||||||
import { useTrack } from 'dashboard/composables';
|
import { useTrack } from 'dashboard/composables';
|
||||||
import NextButton from 'next/button/Button.vue';
|
import NextButton from 'next/button/Button.vue';
|
||||||
|
import NextInput from 'dashboard/components-next/input/Input.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
NextButton,
|
NextButton,
|
||||||
|
NextInput,
|
||||||
},
|
},
|
||||||
directives: {
|
directives: {
|
||||||
onClickOutside: vOnClickOutside,
|
onClickOutside: vOnClickOutside,
|
||||||
@@ -103,20 +105,13 @@ export default {
|
|||||||
{{ $t('FILTER.CUSTOM_VIEWS.ADD.TITLE') }}
|
{{ $t('FILTER.CUSTOM_VIEWS.ADD.TITLE') }}
|
||||||
</h3>
|
</h3>
|
||||||
<form class="w-full grid gap-6" @submit.prevent="saveCustomViews">
|
<form class="w-full grid gap-6" @submit.prevent="saveCustomViews">
|
||||||
<label :class="{ error: v$.name.$error }">
|
<NextInput
|
||||||
<input
|
v-model="name"
|
||||||
v-model="name"
|
:placeholder="$t('FILTER.CUSTOM_VIEWS.ADD.PLACEHOLDER')"
|
||||||
class="py-1.5 px-3 text-n-slate-12 bg-n-alpha-1 text-sm rounded-lg reset-base w-full"
|
:message="v$.name.$error && $t('FILTER.CUSTOM_VIEWS.ADD.ERROR_MESSAGE')"
|
||||||
:placeholder="$t('FILTER.CUSTOM_VIEWS.ADD.PLACEHOLDER')"
|
:message-type="v$.name.$error && 'error'"
|
||||||
@blur="v$.name.$touch"
|
@blur="v$.name.$touch"
|
||||||
/>
|
/>
|
||||||
<span
|
|
||||||
v-if="v$.name.$error"
|
|
||||||
class="text-xs text-n-ruby-11 ml-1 rtl:mr-1"
|
|
||||||
>
|
|
||||||
{{ $t('FILTER.CUSTOM_VIEWS.ADD.ERROR_MESSAGE') }}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div class="flex flex-row justify-end w-full gap-2">
|
<div class="flex flex-row justify-end w-full gap-2">
|
||||||
<NextButton faded slate sm @click.prevent="onClose">
|
<NextButton faded slate sm @click.prevent="onClose">
|
||||||
{{ $t('FILTER.CUSTOM_VIEWS.ADD.CANCEL_BUTTON') }}
|
{{ $t('FILTER.CUSTOM_VIEWS.ADD.CANCEL_BUTTON') }}
|
||||||
|
|||||||
@@ -119,7 +119,13 @@ const handleSeeOriginal = () => {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="isExpandable && !isExpanded"
|
v-if="isExpandable && !isExpanded"
|
||||||
class="absolute left-0 right-0 bottom-0 h-40 px-8 flex items-end bg-gradient-to-t from-n-slate-4 via-n-slate-4 via-20% to-transparent"
|
class="absolute left-0 right-0 bottom-0 h-40 px-8 flex items-end"
|
||||||
|
:class="{
|
||||||
|
'bg-gradient-to-t from-n-slate-4 via-n-slate-4 via-20% to-transparent':
|
||||||
|
isIncoming,
|
||||||
|
'bg-gradient-to-t from-n-solid-blue via-n-solid-blue via-20% to-transparent':
|
||||||
|
isOutgoing,
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="text-n-slate-12 py-2 px-8 mx-auto text-center flex items-center gap-2"
|
class="text-n-slate-12 py-2 px-8 mx-auto text-center flex items-center gap-2"
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
isMobileSidebarOpen: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['toggle']);
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const isConversationRoute = computed(() => {
|
||||||
|
const CONVERSATION_ROUTES = [
|
||||||
|
'inbox_conversation',
|
||||||
|
'conversation_through_inbox',
|
||||||
|
'conversations_through_label',
|
||||||
|
'team_conversations_through_label',
|
||||||
|
'conversations_through_folders',
|
||||||
|
'conversation_through_mentions',
|
||||||
|
'conversation_through_unattended',
|
||||||
|
'conversation_through_participating',
|
||||||
|
'inbox_view_conversation',
|
||||||
|
];
|
||||||
|
return CONVERSATION_ROUTES.includes(route.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleSidebar = () => {
|
||||||
|
emit('toggle');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="!isConversationRoute"
|
||||||
|
id="mobile-sidebar-launcher"
|
||||||
|
class="fixed bottom-4 ltr:left-4 rtl:right-4 z-40 transition-transform duration-200 ease-in-out block md:hidden"
|
||||||
|
:class="[
|
||||||
|
{
|
||||||
|
'ltr:translate-x-48 rtl:-translate-x-48': isMobileSidebarOpen,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div class="rounded-full bg-n-alpha-2 p-1">
|
||||||
|
<Button
|
||||||
|
icon="i-lucide-menu"
|
||||||
|
class="!rounded-full !bg-n-solid-3 dark:!bg-n-alpha-2 !text-n-slate-12 text-xl"
|
||||||
|
lg
|
||||||
|
@click="toggleSidebar"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template v-else />
|
||||||
|
</template>
|
||||||
@@ -8,6 +8,7 @@ import { useStore } from 'vuex';
|
|||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useStorage } from '@vueuse/core';
|
import { useStorage } from '@vueuse/core';
|
||||||
import { useSidebarKeyboardShortcuts } from './useSidebarKeyboardShortcuts';
|
import { useSidebarKeyboardShortcuts } from './useSidebarKeyboardShortcuts';
|
||||||
|
import { vOnClickOutside } from '@vueuse/components';
|
||||||
|
|
||||||
import Button from 'dashboard/components-next/button/Button.vue';
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
import SidebarGroup from './SidebarGroup.vue';
|
import SidebarGroup from './SidebarGroup.vue';
|
||||||
@@ -17,10 +18,18 @@ import SidebarAccountSwitcher from './SidebarAccountSwitcher.vue';
|
|||||||
import Logo from 'next/icon/Logo.vue';
|
import Logo from 'next/icon/Logo.vue';
|
||||||
import ComposeConversation from 'dashboard/components-next/NewConversation/ComposeConversation.vue';
|
import ComposeConversation from 'dashboard/components-next/NewConversation/ComposeConversation.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
isMobileSidebarOpen: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
'closeKeyShortcutModal',
|
'closeKeyShortcutModal',
|
||||||
'openKeyShortcutModal',
|
'openKeyShortcutModal',
|
||||||
'showCreateAccountModal',
|
'showCreateAccountModal',
|
||||||
|
'closeMobileSidebar',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { accountScopedRoute } = useAccount();
|
const { accountScopedRoute } = useAccount();
|
||||||
@@ -77,6 +86,11 @@ const sortedInboxes = computed(() =>
|
|||||||
inboxes.value.slice().sort((a, b) => a.name.localeCompare(b.name))
|
inboxes.value.slice().sort((a, b) => a.name.localeCompare(b.name))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const closeMobileSidebar = () => {
|
||||||
|
if (!props.isMobileSidebarOpen) return;
|
||||||
|
emit('closeMobileSidebar');
|
||||||
|
};
|
||||||
|
|
||||||
const newReportRoutes = () => [
|
const newReportRoutes = () => [
|
||||||
{
|
{
|
||||||
name: 'Reports Agent',
|
name: 'Reports Agent',
|
||||||
@@ -488,7 +502,19 @@ const menuItems = computed(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<aside
|
<aside
|
||||||
class="w-[200px] bg-n-solid-2 rtl:border-l ltr:border-r border-n-weak h-screen flex flex-col text-sm pb-1"
|
v-on-click-outside="[
|
||||||
|
closeMobileSidebar,
|
||||||
|
{ ignore: ['#mobile-sidebar-launcher'] },
|
||||||
|
]"
|
||||||
|
class="bg-n-solid-2 rtl:border-l ltr:border-r border-n-weak flex flex-col text-sm pb-1 fixed top-0 ltr:left-0 rtl:right-0 h-full z-40 transition-transform duration-200 ease-in-out md:static w-[200px] basis-[200px] md:flex-shrink-0 md:ltr:translate-x-0 md:rtl:-translate-x-0"
|
||||||
|
:class="[
|
||||||
|
{
|
||||||
|
'ltr:translate-x-0 rtl:-translate-x-0 shadow-lg md:shadow-none':
|
||||||
|
isMobileSidebarOpen,
|
||||||
|
'ltr:-translate-x-full rtl:translate-x-full md:translate-x-0':
|
||||||
|
!isMobileSidebarOpen,
|
||||||
|
},
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
<section class="grid gap-2 mt-2 mb-4">
|
<section class="grid gap-2 mt-2 mb-4">
|
||||||
<div class="flex items-center min-w-0 gap-2 px-2">
|
<div class="flex items-center min-w-0 gap-2 px-2">
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
} from 'next/dropdown-menu/base';
|
} from 'next/dropdown-menu/base';
|
||||||
import Icon from 'next/icon/Icon.vue';
|
import Icon from 'next/icon/Icon.vue';
|
||||||
import Button from 'next/button/Button.vue';
|
import Button from 'next/button/Button.vue';
|
||||||
|
import ToggleSwitch from 'dashboard/components-next/switch/Switch.vue';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
@@ -48,6 +49,16 @@ const activeStatus = computed(() => {
|
|||||||
return availabilityStatuses.value.find(status => status.active);
|
return availabilityStatuses.value.find(status => status.active);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const autoOfflineToggle = computed({
|
||||||
|
get: () => currentUserAutoOffline.value,
|
||||||
|
set: autoOffline => {
|
||||||
|
store.dispatch('updateAutoOffline', {
|
||||||
|
accountId: currentAccountId.value,
|
||||||
|
autoOffline,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
function changeAvailabilityStatus(availability) {
|
function changeAvailabilityStatus(availability) {
|
||||||
if (isImpersonating.value) {
|
if (isImpersonating.value) {
|
||||||
useAlert(t('PROFILE_SETTINGS.FORM.AVAILABILITY.IMPERSONATING_ERROR'));
|
useAlert(t('PROFILE_SETTINGS.FORM.AVAILABILITY.IMPERSONATING_ERROR'));
|
||||||
@@ -62,13 +73,6 @@ function changeAvailabilityStatus(availability) {
|
|||||||
useAlert(t('PROFILE_SETTINGS.FORM.AVAILABILITY.SET_AVAILABILITY_ERROR'));
|
useAlert(t('PROFILE_SETTINGS.FORM.AVAILABILITY.SET_AVAILABILITY_ERROR'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateAutoOffline(autoOffline) {
|
|
||||||
store.dispatch('updateAutoOffline', {
|
|
||||||
accountId: currentAccountId.value,
|
|
||||||
autoOffline,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -118,11 +122,7 @@ function updateAutoOffline(autoOffline) {
|
|||||||
class="size-4 text-n-slate-10"
|
class="size-4 text-n-slate-10"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<woot-switch
|
<ToggleSwitch v-model="autoOfflineToggle" />
|
||||||
class="flex-shrink-0"
|
|
||||||
:model-value="currentUserAutoOffline"
|
|
||||||
@input="updateAutoOffline"
|
|
||||||
/>
|
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</div>
|
</div>
|
||||||
</DropdownSection>
|
</DropdownSection>
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ const updateValue = () => {
|
|||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="relative h-4 transition-colors duration-200 ease-in-out rounded-full w-7 focus:outline-none focus:ring-1 focus:ring-n-brand focus:ring-offset-n-slate-2 focus:ring-offset-2"
|
class="relative h-4 transition-colors duration-200 ease-in-out rounded-full w-7 focus:outline-none focus:ring-1 focus:ring-n-brand focus:ring-offset-n-slate-2 focus:ring-offset-2 flex-shrink-0"
|
||||||
:class="modelValue ? 'bg-n-brand' : 'bg-n-alpha-1 dark:bg-n-alpha-2'"
|
:class="modelValue ? 'bg-n-brand' : 'bg-n-slate-6 disabled:bg-n-slate-6/60'"
|
||||||
role="switch"
|
role="switch"
|
||||||
:aria-checked="modelValue"
|
:aria-checked="modelValue"
|
||||||
@click="updateValue"
|
@click="updateValue"
|
||||||
|
|||||||
@@ -61,7 +61,9 @@ const onCopy = async e => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="relative text-left">
|
<div class="relative text-left">
|
||||||
<div class="top-1.5 absolute right-1.5 flex items-center gap-1">
|
<div
|
||||||
|
class="top-1.5 absolute ltr:right-1.5 rtl:left-1.5 flex backdrop-blur-sm rounded-lg items-center gap-1"
|
||||||
|
>
|
||||||
<form
|
<form
|
||||||
v-if="enableCodePen"
|
v-if="enableCodePen"
|
||||||
class="flex items-center"
|
class="flex items-center"
|
||||||
@@ -86,6 +88,11 @@ const onCopy = async e => {
|
|||||||
@click="onCopy"
|
@click="onCopy"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<highlightjs v-if="script" :language="lang" :code="scrubbedScript" />
|
<highlightjs
|
||||||
|
v-if="script"
|
||||||
|
:language="lang"
|
||||||
|
:code="scrubbedScript"
|
||||||
|
class="[&_code]:text-start"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
<script>
|
|
||||||
import Banner from 'dashboard/components/ui/Banner.vue';
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
import { useAccount } from 'dashboard/composables/useAccount';
|
|
||||||
import { differenceInDays } from 'date-fns';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: { Banner },
|
|
||||||
setup() {
|
|
||||||
const { accountId } = useAccount();
|
|
||||||
return {
|
|
||||||
accountId,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return { conversationMeta: {} };
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapGetters({
|
|
||||||
isOnChatwootCloud: 'globalConfig/isOnChatwootCloud',
|
|
||||||
getAccount: 'accounts/getAccount',
|
|
||||||
}),
|
|
||||||
bannerMessage() {
|
|
||||||
return this.$t('GENERAL_SETTINGS.LIMITS_UPGRADE');
|
|
||||||
},
|
|
||||||
actionButtonMessage() {
|
|
||||||
return this.$t('GENERAL_SETTINGS.OPEN_BILLING');
|
|
||||||
},
|
|
||||||
shouldShowBanner() {
|
|
||||||
if (!this.isOnChatwootCloud) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isTrialAccount()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.isLimitExceeded();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
if (this.isOnChatwootCloud) {
|
|
||||||
this.fetchLimits();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
fetchLimits() {
|
|
||||||
this.$store.dispatch('accounts/limits');
|
|
||||||
},
|
|
||||||
routeToBilling() {
|
|
||||||
this.$router.push({
|
|
||||||
name: 'billing_settings_index',
|
|
||||||
params: { accountId: this.accountId },
|
|
||||||
});
|
|
||||||
},
|
|
||||||
isTrialAccount() {
|
|
||||||
// check if account is less than 15 days old
|
|
||||||
const account = this.getAccount(this.accountId);
|
|
||||||
if (!account) return false;
|
|
||||||
|
|
||||||
const createdAt = new Date(account.created_at);
|
|
||||||
|
|
||||||
const diffDays = differenceInDays(new Date(), createdAt);
|
|
||||||
|
|
||||||
return diffDays <= 15;
|
|
||||||
},
|
|
||||||
isLimitExceeded() {
|
|
||||||
const account = this.getAccount(this.accountId);
|
|
||||||
if (!account) return false;
|
|
||||||
|
|
||||||
const { limits } = account;
|
|
||||||
if (!limits) return false;
|
|
||||||
|
|
||||||
const { conversation, non_web_inboxes: nonWebInboxes } = limits;
|
|
||||||
return this.testLimit(conversation) || this.testLimit(nonWebInboxes);
|
|
||||||
},
|
|
||||||
testLimit({ allowed, consumed }) {
|
|
||||||
return consumed > allowed;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
|
||||||
<template>
|
|
||||||
<Banner
|
|
||||||
v-if="shouldShowBanner"
|
|
||||||
color-scheme="alert"
|
|
||||||
:banner-message="bannerMessage"
|
|
||||||
:action-button-label="actionButtonMessage"
|
|
||||||
has-action-button
|
|
||||||
@primary-action="routeToBilling"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
@@ -4,7 +4,12 @@ import { useStore } from 'dashboard/composables/store';
|
|||||||
import Copilot from 'dashboard/components-next/copilot/Copilot.vue';
|
import Copilot from 'dashboard/components-next/copilot/Copilot.vue';
|
||||||
import { useMapGetter } from 'dashboard/composables/store';
|
import { useMapGetter } from 'dashboard/composables/store';
|
||||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||||
|
import { useConfig } from 'dashboard/composables/useConfig';
|
||||||
|
import { useWindowSize } from '@vueuse/core';
|
||||||
|
import { vOnClickOutside } from '@vueuse/components';
|
||||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||||
|
import wootConstants from 'dashboard/constants/globals';
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
conversationInboxType: {
|
conversationInboxType: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -13,12 +18,20 @@ defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
const { uiSettings, updateUISettings } = useUISettings();
|
||||||
|
const { isEnterprise } = useConfig();
|
||||||
|
const { width: windowWidth } = useWindowSize();
|
||||||
|
|
||||||
const currentUser = useMapGetter('getCurrentUser');
|
const currentUser = useMapGetter('getCurrentUser');
|
||||||
const assistants = useMapGetter('captainAssistants/getRecords');
|
const assistants = useMapGetter('captainAssistants/getRecords');
|
||||||
const uiFlags = useMapGetter('captainAssistants/getUIFlags');
|
const uiFlags = useMapGetter('captainAssistants/getUIFlags');
|
||||||
const inboxAssistant = useMapGetter('getCopilotAssistant');
|
const inboxAssistant = useMapGetter('getCopilotAssistant');
|
||||||
const currentChat = useMapGetter('getSelectedChat');
|
const currentChat = useMapGetter('getSelectedChat');
|
||||||
|
|
||||||
|
const isSmallScreen = computed(
|
||||||
|
() => windowWidth.value < wootConstants.SMALL_SCREEN_BREAKPOINT
|
||||||
|
);
|
||||||
|
|
||||||
const selectedCopilotThreadId = ref(null);
|
const selectedCopilotThreadId = ref(null);
|
||||||
const messages = computed(() =>
|
const messages = computed(() =>
|
||||||
store.getters['copilotMessages/getMessagesByThreadId'](
|
store.getters['copilotMessages/getMessagesByThreadId'](
|
||||||
@@ -32,7 +45,6 @@ const isFeatureEnabledonAccount = useMapGetter(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const selectedAssistantId = ref(null);
|
const selectedAssistantId = ref(null);
|
||||||
const { uiSettings, updateUISettings } = useUISettings();
|
|
||||||
|
|
||||||
const activeAssistant = computed(() => {
|
const activeAssistant = computed(() => {
|
||||||
const preferredId = uiSettings.value.preferred_captain_assistant_id;
|
const preferredId = uiSettings.value.preferred_captain_assistant_id;
|
||||||
@@ -55,6 +67,15 @@ const activeAssistant = computed(() => {
|
|||||||
return assistants.value[0];
|
return assistants.value[0];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const closeCopilotPanel = () => {
|
||||||
|
if (isSmallScreen.value && uiSettings.value?.is_copilot_panel_open) {
|
||||||
|
updateUISettings({
|
||||||
|
is_contact_sidebar_open: false,
|
||||||
|
is_copilot_panel_open: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const setAssistant = async assistant => {
|
const setAssistant = async assistant => {
|
||||||
selectedAssistantId.value = assistant.id;
|
selectedAssistantId.value = assistant.id;
|
||||||
await updateUISettings({
|
await updateUISettings({
|
||||||
@@ -63,6 +84,9 @@ const setAssistant = async assistant => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const shouldShowCopilotPanel = computed(() => {
|
const shouldShowCopilotPanel = computed(() => {
|
||||||
|
if (!isEnterprise) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const isCaptainEnabled = isFeatureEnabledonAccount.value(
|
const isCaptainEnabled = isFeatureEnabledonAccount.value(
|
||||||
currentAccountId.value,
|
currentAccountId.value,
|
||||||
FEATURE_FLAGS.CAPTAIN
|
FEATURE_FLAGS.CAPTAIN
|
||||||
@@ -94,14 +118,23 @@ const sendMessage = async message => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
store.dispatch('captainAssistants/get');
|
if (isEnterprise) {
|
||||||
|
store.dispatch('captainAssistants/get');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="shouldShowCopilotPanel"
|
v-if="shouldShowCopilotPanel"
|
||||||
class="ltr:border-l rtl:border-r border-n-weak h-full overflow-hidden z-10 w-[320px] min-w-[320px] 2xl:min-w-[360px] 2xl:w-[360px] flex flex-col bg-n-background"
|
v-on-click-outside="() => closeCopilotPanel()"
|
||||||
|
class="bg-n-background h-full overflow-hidden flex-col fixed top-0 ltr:right-0 rtl:left-0 z-40 w-full max-w-sm transition-transform duration-300 ease-in-out md:static md:w-[320px] md:min-w-[320px] ltr:border-l rtl:border-r border-n-weak 2xl:min-w-[360px] 2xl:w-[360px] shadow-lg md:shadow-none"
|
||||||
|
:class="[
|
||||||
|
{
|
||||||
|
'md:flex': shouldShowCopilotPanel,
|
||||||
|
'md:hidden': !shouldShowCopilotPanel,
|
||||||
|
},
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
<Copilot
|
<Copilot
|
||||||
:messages="messages"
|
:messages="messages"
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
modelValue: { type: Boolean, default: false },
|
|
||||||
size: { type: String, default: '' },
|
|
||||||
},
|
|
||||||
emits: ['update:modelValue', 'input'],
|
|
||||||
methods: {
|
|
||||||
onClick() {
|
|
||||||
this.$emit('update:modelValue', !this.modelValue);
|
|
||||||
this.$emit('input', !this.modelValue);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="toggle-button p-0"
|
|
||||||
:class="{ active: modelValue, small: size === 'small' }"
|
|
||||||
role="switch"
|
|
||||||
:aria-checked="modelValue.toString()"
|
|
||||||
@click="onClick"
|
|
||||||
>
|
|
||||||
<span aria-hidden="true" :class="{ active: modelValue }" />
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.toggle-button {
|
|
||||||
@apply bg-n-slate-5;
|
|
||||||
--toggle-button-box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px,
|
|
||||||
rgba(59, 130, 246, 0.5) 0px 0px 0px 0px, rgba(0, 0, 0, 0.1) 0px 1px 3px 0px,
|
|
||||||
rgba(0, 0, 0, 0.06) 0px 1px 2px 0px;
|
|
||||||
border-radius: 0.5625rem;
|
|
||||||
border: 2px solid transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
height: 1.188rem;
|
|
||||||
position: relative;
|
|
||||||
transition-duration: 200ms;
|
|
||||||
transition-property: background-color;
|
|
||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
width: 2.125rem;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
@apply bg-n-brand;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.small {
|
|
||||||
width: 1.375rem;
|
|
||||||
height: 0.875rem;
|
|
||||||
|
|
||||||
span {
|
|
||||||
@apply size-2.5;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
@apply ltr:translate-x-[0.5rem] ltr:translate-y-0 rtl:translate-x-[-0.5rem] rtl:translate-y-0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
@apply bg-n-background;
|
|
||||||
|
|
||||||
border-radius: 100%;
|
|
||||||
box-shadow: var(--toggle-button-box-shadow);
|
|
||||||
display: inline-block;
|
|
||||||
height: 0.9375rem;
|
|
||||||
transform: translate(0, 0);
|
|
||||||
transition-duration: 200ms;
|
|
||||||
transition-property: transform;
|
|
||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
width: 0.9375rem;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
@apply ltr:translate-x-[0.9375rem] ltr:translate-y-0 rtl:translate-x-[-0.9375rem] rtl:translate-y-0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -15,6 +15,7 @@ import CannedResponse from '../conversation/CannedResponse.vue';
|
|||||||
import KeyboardEmojiSelector from './keyboardEmojiSelector.vue';
|
import KeyboardEmojiSelector from './keyboardEmojiSelector.vue';
|
||||||
import TagAgents from '../conversation/TagAgents.vue';
|
import TagAgents from '../conversation/TagAgents.vue';
|
||||||
import VariableList from '../conversation/VariableList.vue';
|
import VariableList from '../conversation/VariableList.vue';
|
||||||
|
import TagTools from '../conversation/TagTools.vue';
|
||||||
|
|
||||||
import { useEmitter } from 'dashboard/composables/emitter';
|
import { useEmitter } from 'dashboard/composables/emitter';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
@@ -72,6 +73,7 @@ const props = defineProps({
|
|||||||
updateSelectionWith: { type: String, default: '' },
|
updateSelectionWith: { type: String, default: '' },
|
||||||
enableVariables: { type: Boolean, default: false },
|
enableVariables: { type: Boolean, default: false },
|
||||||
enableCannedResponses: { type: Boolean, default: true },
|
enableCannedResponses: { type: Boolean, default: true },
|
||||||
|
enableCaptainTools: { type: Boolean, default: false },
|
||||||
variables: { type: Object, default: () => ({}) },
|
variables: { type: Object, default: () => ({}) },
|
||||||
enabledMenuOptions: { type: Array, default: () => [] },
|
enabledMenuOptions: { type: Array, default: () => [] },
|
||||||
signature: { type: String, default: '' },
|
signature: { type: String, default: '' },
|
||||||
@@ -89,6 +91,7 @@ const emit = defineEmits([
|
|||||||
'toggleUserMention',
|
'toggleUserMention',
|
||||||
'toggleCannedMenu',
|
'toggleCannedMenu',
|
||||||
'toggleVariablesMenu',
|
'toggleVariablesMenu',
|
||||||
|
'toggleToolsMenu',
|
||||||
'clearSelection',
|
'clearSelection',
|
||||||
'blur',
|
'blur',
|
||||||
'focus',
|
'focus',
|
||||||
@@ -140,7 +143,9 @@ const showUserMentions = ref(false);
|
|||||||
const showCannedMenu = ref(false);
|
const showCannedMenu = ref(false);
|
||||||
const showVariables = ref(false);
|
const showVariables = ref(false);
|
||||||
const showEmojiMenu = ref(false);
|
const showEmojiMenu = ref(false);
|
||||||
|
const showToolsMenu = ref(false);
|
||||||
const mentionSearchKey = ref('');
|
const mentionSearchKey = ref('');
|
||||||
|
const toolSearchKey = ref('');
|
||||||
const cannedSearchTerm = ref('');
|
const cannedSearchTerm = ref('');
|
||||||
const variableSearchTerm = ref('');
|
const variableSearchTerm = ref('');
|
||||||
const emojiSearchTerm = ref('');
|
const emojiSearchTerm = ref('');
|
||||||
@@ -216,11 +221,17 @@ const plugins = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
createSuggestionPlugin({
|
||||||
|
trigger: '@',
|
||||||
|
showMenu: showToolsMenu,
|
||||||
|
searchTerm: toolSearchKey,
|
||||||
|
isAllowed: () => props.enableCaptainTools,
|
||||||
|
}),
|
||||||
createSuggestionPlugin({
|
createSuggestionPlugin({
|
||||||
trigger: '@',
|
trigger: '@',
|
||||||
showMenu: showUserMentions,
|
showMenu: showUserMentions,
|
||||||
searchTerm: mentionSearchKey,
|
searchTerm: mentionSearchKey,
|
||||||
isAllowed: () => props.isPrivate,
|
isAllowed: () => props.isPrivate || !props.enableCaptainTools,
|
||||||
}),
|
}),
|
||||||
createSuggestionPlugin({
|
createSuggestionPlugin({
|
||||||
trigger: '/',
|
trigger: '/',
|
||||||
@@ -262,6 +273,9 @@ watch(showCannedMenu, updatedValue => {
|
|||||||
watch(showVariables, updatedValue => {
|
watch(showVariables, updatedValue => {
|
||||||
emit('toggleVariablesMenu', !props.isPrivate && updatedValue);
|
emit('toggleVariablesMenu', !props.isPrivate && updatedValue);
|
||||||
});
|
});
|
||||||
|
watch(showToolsMenu, updatedValue => {
|
||||||
|
emit('toggleToolsMenu', props.enableCaptainTools && updatedValue);
|
||||||
|
});
|
||||||
|
|
||||||
function focusEditorInputField(pos = 'end') {
|
function focusEditorInputField(pos = 'end') {
|
||||||
const { tr } = editorView.state;
|
const { tr } = editorView.state;
|
||||||
@@ -538,6 +552,7 @@ function insertSpecialContent(type, content) {
|
|||||||
cannedResponse: CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE,
|
cannedResponse: CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE,
|
||||||
variable: CONVERSATION_EVENTS.INSERTED_A_VARIABLE,
|
variable: CONVERSATION_EVENTS.INSERTED_A_VARIABLE,
|
||||||
emoji: CONVERSATION_EVENTS.INSERTED_AN_EMOJI,
|
emoji: CONVERSATION_EVENTS.INSERTED_AN_EMOJI,
|
||||||
|
tool: CONVERSATION_EVENTS.INSERTED_A_TOOL,
|
||||||
};
|
};
|
||||||
|
|
||||||
useTrack(event_map[type]);
|
useTrack(event_map[type]);
|
||||||
@@ -699,6 +714,11 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
|
|||||||
:search-key="emojiSearchTerm"
|
:search-key="emojiSearchTerm"
|
||||||
@select-emoji="emoji => insertSpecialContent('emoji', emoji)"
|
@select-emoji="emoji => insertSpecialContent('emoji', emoji)"
|
||||||
/>
|
/>
|
||||||
|
<TagTools
|
||||||
|
v-if="showToolsMenu"
|
||||||
|
:search-key="toolSearchKey"
|
||||||
|
@select-tool="content => insertSpecialContent('tool', content)"
|
||||||
|
/>
|
||||||
<input
|
<input
|
||||||
ref="imageUpload"
|
ref="imageUpload"
|
||||||
type="file"
|
type="file"
|
||||||
@@ -812,6 +832,10 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.prosemirror-tools-node {
|
||||||
|
@apply font-medium text-n-slate-12 py-0;
|
||||||
|
}
|
||||||
|
|
||||||
.editor-wrap {
|
.editor-wrap {
|
||||||
@apply mb-4;
|
@apply mb-4;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
hasWhatsappTemplates: {
|
enableWhatsAppTemplates: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
@@ -333,7 +333,7 @@ export default {
|
|||||||
@click="toggleMessageSignature"
|
@click="toggleMessageSignature"
|
||||||
/>
|
/>
|
||||||
<NextButton
|
<NextButton
|
||||||
v-if="hasWhatsappTemplates"
|
v-if="enableWhatsAppTemplates"
|
||||||
v-tooltip.top-end="$t('CONVERSATION.FOOTER.WHATSAPP_TEMPLATES')"
|
v-tooltip.top-end="$t('CONVERSATION.FOOTER.WHATSAPP_TEMPLATES')"
|
||||||
icon="i-ph-whatsapp-logo"
|
icon="i-ph-whatsapp-logo"
|
||||||
slate
|
slate
|
||||||
|
|||||||
@@ -68,6 +68,10 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
allowedContextMenuOptions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
emits: [
|
emits: [
|
||||||
'contextMenuToggle',
|
'contextMenuToggle',
|
||||||
@@ -151,11 +155,9 @@ export default {
|
|||||||
hasSlaPolicyId() {
|
hasSlaPolicyId() {
|
||||||
return this.chat?.sla_policy_id;
|
return this.chat?.sla_policy_id;
|
||||||
},
|
},
|
||||||
},
|
conversationPath() {
|
||||||
methods: {
|
|
||||||
onCardClick(e) {
|
|
||||||
const { activeInbox, chat } = this;
|
const { activeInbox, chat } = this;
|
||||||
const path = frontendURL(
|
return frontendURL(
|
||||||
conversationUrl({
|
conversationUrl({
|
||||||
accountId: this.accountId,
|
accountId: this.accountId,
|
||||||
activeInbox,
|
activeInbox,
|
||||||
@@ -166,18 +168,26 @@ export default {
|
|||||||
conversationType: this.conversationType,
|
conversationType: this.conversationType,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onCardClick(e) {
|
||||||
|
const path = this.conversationPath;
|
||||||
|
if (!path) return;
|
||||||
|
|
||||||
|
// Handle Ctrl/Cmd + Click for new tab
|
||||||
if (e.metaKey || e.ctrlKey) {
|
if (e.metaKey || e.ctrlKey) {
|
||||||
|
e.preventDefault();
|
||||||
window.open(
|
window.open(
|
||||||
window.chatwootConfig.hostURL + path,
|
`${window.chatwootConfig.hostURL}${path}`,
|
||||||
'_blank',
|
'_blank',
|
||||||
'noopener noreferrer nofollow'
|
'noopener,noreferrer'
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.isActiveChat) {
|
|
||||||
return;
|
// Skip if already active
|
||||||
}
|
if (this.isActiveChat) return;
|
||||||
|
|
||||||
router.push({ path });
|
router.push({ path });
|
||||||
},
|
},
|
||||||
@@ -359,6 +369,8 @@ export default {
|
|||||||
:priority="chat.priority"
|
:priority="chat.priority"
|
||||||
:chat-id="chat.id"
|
:chat-id="chat.id"
|
||||||
:has-unread-messages="hasUnread"
|
:has-unread-messages="hasUnread"
|
||||||
|
:conversation-url="conversationPath"
|
||||||
|
:allowed-options="allowedContextMenuOptions"
|
||||||
@update-conversation="onUpdateConversation"
|
@update-conversation="onUpdateConversation"
|
||||||
@assign-agent="onAssignAgent"
|
@assign-agent="onAssignAgent"
|
||||||
@assign-label="onAssignLabel"
|
@assign-label="onAssignLabel"
|
||||||
@@ -367,6 +379,7 @@ export default {
|
|||||||
@mark-as-read="markAsRead"
|
@mark-as-read="markAsRead"
|
||||||
@assign-priority="assignPriority"
|
@assign-priority="assignPriority"
|
||||||
@delete-conversation="deleteConversation"
|
@delete-conversation="deleteConversation"
|
||||||
|
@close="closeContextMenu"
|
||||||
/>
|
/>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import ContactPanel from 'dashboard/routes/dashboard/conversation/ContactPanel.vue';
|
import ContactPanel from 'dashboard/routes/dashboard/conversation/ContactPanel.vue';
|
||||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||||
|
import { useWindowSize } from '@vueuse/core';
|
||||||
|
import { vOnClickOutside } from '@vueuse/components';
|
||||||
|
import wootConstants from 'dashboard/constants/globals';
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
currentChat: {
|
currentChat: {
|
||||||
@@ -10,7 +13,8 @@ defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { uiSettings } = useUISettings();
|
const { uiSettings, updateUISettings } = useUISettings();
|
||||||
|
const { width: windowWidth } = useWindowSize();
|
||||||
|
|
||||||
const activeTab = computed(() => {
|
const activeTab = computed(() => {
|
||||||
const { is_contact_sidebar_open: isContactSidebarOpen } = uiSettings.value;
|
const { is_contact_sidebar_open: isContactSidebarOpen } = uiSettings.value;
|
||||||
@@ -20,11 +24,31 @@ const activeTab = computed(() => {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isSmallScreen = computed(
|
||||||
|
() => windowWidth.value < wootConstants.SMALL_SCREEN_BREAKPOINT
|
||||||
|
);
|
||||||
|
|
||||||
|
const closeContactPanel = () => {
|
||||||
|
if (isSmallScreen.value && uiSettings.value?.is_contact_sidebar_open) {
|
||||||
|
updateUISettings({
|
||||||
|
is_contact_sidebar_open: false,
|
||||||
|
is_copilot_panel_open: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="ltr:border-l rtl:border-r border-n-weak h-full overflow-hidden z-10 w-[320px] min-w-[320px] 2xl:min-w-[360px] 2xl:w-[360px] flex flex-col bg-n-background"
|
v-on-click-outside="() => closeContactPanel()"
|
||||||
|
class="bg-n-background h-full overflow-hidden flex flex-col fixed top-0 z-40 w-full max-w-sm transition-transform duration-300 ease-in-out ltr:right-0 rtl:left-0 md:static md:w-[320px] md:min-w-[320px] ltr:border-l rtl:border-r border-n-weak 2xl:min-w-[360px] 2xl:w-[360px] shadow-lg md:shadow-none"
|
||||||
|
:class="[
|
||||||
|
{
|
||||||
|
'md:flex': activeTab === 0,
|
||||||
|
'md:hidden': activeTab !== 0,
|
||||||
|
},
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
<div class="flex flex-1 overflow-auto">
|
<div class="flex flex-1 overflow-auto">
|
||||||
<ContactPanel
|
<ContactPanel
|
||||||
|
|||||||
@@ -184,9 +184,8 @@ export default {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
hasWhatsappTemplates() {
|
showWhatsappTemplates() {
|
||||||
return !!this.$store.getters['inboxes/getWhatsAppTemplates'](this.inboxId)
|
return this.isAWhatsAppCloudChannel && !this.isPrivate;
|
||||||
.length;
|
|
||||||
},
|
},
|
||||||
isPrivate() {
|
isPrivate() {
|
||||||
if (this.currentChat.can_reply || this.isAWhatsAppChannel) {
|
if (this.currentChat.can_reply || this.isAWhatsAppChannel) {
|
||||||
@@ -373,6 +372,7 @@ export default {
|
|||||||
const variables = getMessageVariables({
|
const variables = getMessageVariables({
|
||||||
conversation: this.currentChat,
|
conversation: this.currentChat,
|
||||||
contact: this.currentContact,
|
contact: this.currentContact,
|
||||||
|
inbox: this.inbox,
|
||||||
});
|
});
|
||||||
return variables;
|
return variables;
|
||||||
},
|
},
|
||||||
@@ -1212,7 +1212,7 @@ export default {
|
|||||||
<ReplyBottomPanel
|
<ReplyBottomPanel
|
||||||
:conversation-id="conversationId"
|
:conversation-id="conversationId"
|
||||||
:enable-multiple-file-upload="enableMultipleFileUpload"
|
:enable-multiple-file-upload="enableMultipleFileUpload"
|
||||||
:has-whatsapp-templates="hasWhatsappTemplates"
|
:enable-whats-app-templates="showWhatsappTemplates"
|
||||||
:inbox="inbox"
|
:inbox="inbox"
|
||||||
:is-on-private-note="isOnPrivateNote"
|
:is-on-private-note="isOnPrivateNote"
|
||||||
:is-recording-audio="isRecordingAudio"
|
:is-recording-audio="isRecordingAudio"
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch } from 'vue';
|
||||||
|
import ToolsDropdown from 'dashboard/components-next/captain/assistant/ToolsDropdown.vue';
|
||||||
|
import { useKeyboardNavigableList } from 'dashboard/composables/useKeyboardNavigableList';
|
||||||
|
import { useMapGetter } from 'dashboard/composables/store.js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
searchKey: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['selectTool']);
|
||||||
|
|
||||||
|
const tools = useMapGetter('captainTools/getRecords');
|
||||||
|
|
||||||
|
const selectedIndex = ref(0);
|
||||||
|
|
||||||
|
const filteredTools = computed(() => {
|
||||||
|
const search = props.searchKey?.trim().toLowerCase() || '';
|
||||||
|
|
||||||
|
return tools.value.filter(tool => tool.title.toLowerCase().includes(search));
|
||||||
|
});
|
||||||
|
|
||||||
|
const adjustScroll = () => {};
|
||||||
|
|
||||||
|
const onSelect = idx => {
|
||||||
|
if (idx) selectedIndex.value = idx;
|
||||||
|
emit('selectTool', filteredTools.value[selectedIndex.value]);
|
||||||
|
};
|
||||||
|
|
||||||
|
useKeyboardNavigableList({
|
||||||
|
items: filteredTools,
|
||||||
|
onSelect,
|
||||||
|
adjustScroll,
|
||||||
|
selectedIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(filteredTools, newListOfTools => {
|
||||||
|
if (newListOfTools.length < selectedIndex.value + 1) {
|
||||||
|
selectedIndex.value = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ToolsDropdown
|
||||||
|
v-if="filteredTools.length"
|
||||||
|
:items="filteredTools"
|
||||||
|
:selected-index="selectedIndex"
|
||||||
|
class="bottom-20"
|
||||||
|
@select="onSelect"
|
||||||
|
/>
|
||||||
|
<template v-else />
|
||||||
|
</template>
|
||||||
+56
-18
@@ -1,8 +1,13 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||||
// TODO: Remove this when we support all formats
|
// TODO: Remove this when we support all formats
|
||||||
const formatsToRemove = ['DOCUMENT', 'IMAGE', 'VIDEO'];
|
const formatsToRemove = ['DOCUMENT', 'IMAGE', 'VIDEO'];
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
Icon,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
inboxId: {
|
inboxId: {
|
||||||
type: Number,
|
type: Number,
|
||||||
@@ -13,6 +18,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
query: '',
|
query: '',
|
||||||
|
isRefreshing: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -37,38 +43,63 @@ export default {
|
|||||||
return template.components.find(component => component.type === 'BODY')
|
return template.components.find(component => component.type === 'BODY')
|
||||||
.text;
|
.text;
|
||||||
},
|
},
|
||||||
|
async refreshTemplates() {
|
||||||
|
this.isRefreshing = true;
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch('inboxes/syncTemplates', this.inboxId);
|
||||||
|
useAlert(this.$t('WHATSAPP_TEMPLATES.PICKER.REFRESH_SUCCESS'));
|
||||||
|
} catch (error) {
|
||||||
|
useAlert(this.$t('WHATSAPP_TEMPLATES.PICKER.REFRESH_ERROR'));
|
||||||
|
} finally {
|
||||||
|
this.isRefreshing = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<div
|
<div class="flex gap-2 mb-2.5">
|
||||||
class="gap-1 bg-n-alpha-black2 items-center flex mb-2.5 py-0 px-2.5 rounded-lg outline outline-1 outline-n-weak hover:outline-n-slate-6 dark:hover:outline-n-slate-6 focus-within:outline-n-brand dark:focus-within:outline-n-brand"
|
<div
|
||||||
>
|
class="flex flex-1 gap-1 items-center px-2.5 py-0 rounded-lg bg-n-alpha-black2 outline outline-1 outline-n-weak hover:outline-n-slate-6 dark:hover:outline-n-slate-6 focus-within:outline-n-brand dark:focus-within:outline-n-brand"
|
||||||
<fluent-icon icon="search" class="text-n-slate-12" size="16" />
|
>
|
||||||
<input
|
<fluent-icon icon="search" class="text-n-slate-12" size="16" />
|
||||||
v-model="query"
|
<input
|
||||||
type="search"
|
v-model="query"
|
||||||
:placeholder="$t('WHATSAPP_TEMPLATES.PICKER.SEARCH_PLACEHOLDER')"
|
type="search"
|
||||||
class="reset-base w-full h-9 bg-transparent text-n-slate-12 !text-sm !outline-0"
|
:placeholder="$t('WHATSAPP_TEMPLATES.PICKER.SEARCH_PLACEHOLDER')"
|
||||||
/>
|
class="reset-base w-full h-9 bg-transparent text-n-slate-12 !text-sm !outline-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
:disabled="isRefreshing"
|
||||||
|
class="flex justify-center items-center w-9 h-9 rounded-lg bg-n-alpha-black2 outline outline-1 outline-n-weak hover:outline-n-slate-6 dark:hover:outline-n-slate-6 hover:bg-n-alpha-2 dark:hover:bg-n-solid-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
:title="$t('WHATSAPP_TEMPLATES.PICKER.REFRESH_BUTTON')"
|
||||||
|
@click="refreshTemplates"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon="i-lucide-refresh-ccw"
|
||||||
|
class="text-n-slate-12 size-4"
|
||||||
|
:class="{ 'animate-spin': isRefreshing }"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="bg-n-background outline-n-container outline outline-1 rounded-lg max-h-[18.75rem] overflow-y-auto p-2.5"
|
class="bg-n-background outline-n-container outline outline-1 rounded-lg max-h-[18.75rem] overflow-y-auto p-2.5"
|
||||||
>
|
>
|
||||||
<div v-for="(template, i) in filteredTemplateMessages" :key="template.id">
|
<div v-for="(template, i) in filteredTemplateMessages" :key="template.id">
|
||||||
<button
|
<button
|
||||||
class="rounded-lg cursor-pointer block p-2.5 text-left w-full hover:bg-n-alpha-2 dark:hover:bg-n-solid-2"
|
class="block p-2.5 w-full text-left rounded-lg cursor-pointer hover:bg-n-alpha-2 dark:hover:bg-n-solid-2"
|
||||||
@click="$emit('onSelect', template)"
|
@click="$emit('onSelect', template)"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center justify-between mb-2.5">
|
<div class="flex justify-between items-center mb-2.5">
|
||||||
<p class="text-sm">
|
<p class="text-sm">
|
||||||
{{ template.name }}
|
{{ template.name }}
|
||||||
</p>
|
</p>
|
||||||
<span
|
<span
|
||||||
class="inline-block px-2 py-1 text-xs leading-none bg-n-slate-3 rounded-lg cursor-default text-n-slate-12"
|
class="inline-block px-2 py-1 text-xs leading-none rounded-lg cursor-default bg-n-slate-3 text-n-slate-12"
|
||||||
>
|
>
|
||||||
{{ $t('WHATSAPP_TEMPLATES.PICKER.LABELS.LANGUAGE') }} :
|
{{ $t('WHATSAPP_TEMPLATES.PICKER.LABELS.LANGUAGE') }} :
|
||||||
{{ template.language }}
|
{{ template.language }}
|
||||||
@@ -94,11 +125,18 @@ export default {
|
|||||||
class="border-b border-solid border-n-weak my-2.5 mx-auto max-w-[95%]"
|
class="border-b border-solid border-n-weak my-2.5 mx-auto max-w-[95%]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!filteredTemplateMessages.length">
|
<div v-if="!filteredTemplateMessages.length" class="py-8 text-center">
|
||||||
<p>
|
<div v-if="query && whatsAppTemplateMessages.length">
|
||||||
{{ $t('WHATSAPP_TEMPLATES.PICKER.NO_TEMPLATES_FOUND') }}
|
<p>
|
||||||
<strong>{{ query }}</strong>
|
{{ $t('WHATSAPP_TEMPLATES.PICKER.NO_TEMPLATES_FOUND') }}
|
||||||
</p>
|
<strong>{{ query }}</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="!whatsAppTemplateMessages.length" class="space-y-4">
|
||||||
|
<p class="text-n-slate-11">
|
||||||
|
{{ $t('WHATSAPP_TEMPLATES.PICKER.NO_TEMPLATES_AVAILABLE') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||||
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
||||||
import {
|
import {
|
||||||
getSortedAgentsByAvailability,
|
getSortedAgentsByAvailability,
|
||||||
getAgentsByUpdatedPresence,
|
getAgentsByUpdatedPresence,
|
||||||
@@ -8,7 +11,20 @@ import MenuItem from './menuItem.vue';
|
|||||||
import MenuItemWithSubmenu from './menuItemWithSubmenu.vue';
|
import MenuItemWithSubmenu from './menuItemWithSubmenu.vue';
|
||||||
import wootConstants from 'dashboard/constants/globals';
|
import wootConstants from 'dashboard/constants/globals';
|
||||||
import AgentLoadingPlaceholder from './agentLoadingPlaceholder.vue';
|
import AgentLoadingPlaceholder from './agentLoadingPlaceholder.vue';
|
||||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
|
||||||
|
const MENU = {
|
||||||
|
MARK_AS_READ: 'mark-as-read',
|
||||||
|
MARK_AS_UNREAD: 'mark-as-unread',
|
||||||
|
PRIORITY: 'priority',
|
||||||
|
STATUS: 'status',
|
||||||
|
SNOOZE: 'snooze',
|
||||||
|
AGENT: 'agent',
|
||||||
|
TEAM: 'team',
|
||||||
|
LABEL: 'label',
|
||||||
|
DELETE: 'delete',
|
||||||
|
OPEN_NEW_TAB: 'open-new-tab',
|
||||||
|
COPY_LINK: 'copy-link',
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -37,6 +53,14 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
conversationUrl: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
allowedOptions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
emits: [
|
emits: [
|
||||||
'updateConversation',
|
'updateConversation',
|
||||||
@@ -47,6 +71,7 @@ export default {
|
|||||||
'assignTeam',
|
'assignTeam',
|
||||||
'assignLabel',
|
'assignLabel',
|
||||||
'deleteConversation',
|
'deleteConversation',
|
||||||
|
'close',
|
||||||
],
|
],
|
||||||
setup() {
|
setup() {
|
||||||
const { isAdmin } = useAdmin();
|
const { isAdmin } = useAdmin();
|
||||||
@@ -56,6 +81,7 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
MENU,
|
||||||
STATUS_TYPE: wootConstants.STATUS_TYPE,
|
STATUS_TYPE: wootConstants.STATUS_TYPE,
|
||||||
readOption: {
|
readOption: {
|
||||||
label: this.$t('CONVERSATION.CARD_CONTEXT_MENU.MARK_AS_READ'),
|
label: this.$t('CONVERSATION.CARD_CONTEXT_MENU.MARK_AS_READ'),
|
||||||
@@ -63,7 +89,7 @@ export default {
|
|||||||
},
|
},
|
||||||
unreadOption: {
|
unreadOption: {
|
||||||
label: this.$t('CONVERSATION.CARD_CONTEXT_MENU.MARK_AS_UNREAD'),
|
label: this.$t('CONVERSATION.CARD_CONTEXT_MENU.MARK_AS_UNREAD'),
|
||||||
icon: 'mail',
|
icon: 'mail-unread',
|
||||||
},
|
},
|
||||||
statusMenuConfig: [
|
statusMenuConfig: [
|
||||||
{
|
{
|
||||||
@@ -88,7 +114,7 @@ export default {
|
|||||||
icon: 'snooze',
|
icon: 'snooze',
|
||||||
},
|
},
|
||||||
priorityConfig: {
|
priorityConfig: {
|
||||||
key: 'priority',
|
key: MENU.PRIORITY,
|
||||||
label: this.$t('CONVERSATION.PRIORITY.TITLE'),
|
label: this.$t('CONVERSATION.PRIORITY.TITLE'),
|
||||||
icon: 'warning',
|
icon: 'warning',
|
||||||
options: [
|
options: [
|
||||||
@@ -115,25 +141,35 @@ export default {
|
|||||||
].filter(item => item.key !== this.priority),
|
].filter(item => item.key !== this.priority),
|
||||||
},
|
},
|
||||||
labelMenuConfig: {
|
labelMenuConfig: {
|
||||||
key: 'label',
|
key: MENU.LABEL,
|
||||||
icon: 'tag',
|
icon: 'tag',
|
||||||
label: this.$t('CONVERSATION.CARD_CONTEXT_MENU.ASSIGN_LABEL'),
|
label: this.$t('CONVERSATION.CARD_CONTEXT_MENU.ASSIGN_LABEL'),
|
||||||
},
|
},
|
||||||
agentMenuConfig: {
|
agentMenuConfig: {
|
||||||
key: 'agent',
|
key: MENU.AGENT,
|
||||||
icon: 'person-add',
|
icon: 'person-add',
|
||||||
label: this.$t('CONVERSATION.CARD_CONTEXT_MENU.ASSIGN_AGENT'),
|
label: this.$t('CONVERSATION.CARD_CONTEXT_MENU.ASSIGN_AGENT'),
|
||||||
},
|
},
|
||||||
teamMenuConfig: {
|
teamMenuConfig: {
|
||||||
key: 'team',
|
key: MENU.TEAM,
|
||||||
icon: 'people-team-add',
|
icon: 'people-team-add',
|
||||||
label: this.$t('CONVERSATION.CARD_CONTEXT_MENU.ASSIGN_TEAM'),
|
label: this.$t('CONVERSATION.CARD_CONTEXT_MENU.ASSIGN_TEAM'),
|
||||||
},
|
},
|
||||||
deleteOption: {
|
deleteOption: {
|
||||||
key: 'delete',
|
key: MENU.DELETE,
|
||||||
icon: 'delete',
|
icon: 'delete',
|
||||||
label: this.$t('CONVERSATION.CARD_CONTEXT_MENU.DELETE'),
|
label: this.$t('CONVERSATION.CARD_CONTEXT_MENU.DELETE'),
|
||||||
},
|
},
|
||||||
|
openInNewTabOption: {
|
||||||
|
key: MENU.OPEN_NEW_TAB,
|
||||||
|
icon: 'open',
|
||||||
|
label: this.$t('CONVERSATION.CARD_CONTEXT_MENU.OPEN_IN_NEW_TAB'),
|
||||||
|
},
|
||||||
|
copyLinkOption: {
|
||||||
|
key: MENU.COPY_LINK,
|
||||||
|
icon: 'copy',
|
||||||
|
label: this.$t('CONVERSATION.CARD_CONTEXT_MENU.COPY_LINK'),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -180,6 +216,10 @@ export default {
|
|||||||
this.$store.dispatch('inboxAssignableAgents/fetch', [this.inboxId]);
|
this.$store.dispatch('inboxAssignableAgents/fetch', [this.inboxId]);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
isAllowed(keys) {
|
||||||
|
if (!this.allowedOptions.length) return true;
|
||||||
|
return keys.some(key => this.allowedOptions.includes(key));
|
||||||
|
},
|
||||||
toggleStatus(status, snoozedUntil) {
|
toggleStatus(status, snoozedUntil) {
|
||||||
this.$emit('updateConversation', status, snoozedUntil);
|
this.$emit('updateConversation', status, snoozedUntil);
|
||||||
},
|
},
|
||||||
@@ -194,6 +234,24 @@ export default {
|
|||||||
deleteConversation() {
|
deleteConversation() {
|
||||||
this.$emit('deleteConversation', this.chatId);
|
this.$emit('deleteConversation', this.chatId);
|
||||||
},
|
},
|
||||||
|
openInNewTab() {
|
||||||
|
if (!this.conversationUrl) return;
|
||||||
|
|
||||||
|
const url = `${window.chatwootConfig.hostURL}${this.conversationUrl}`;
|
||||||
|
window.open(url, '_blank', 'noopener,noreferrer');
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
async copyConversationLink() {
|
||||||
|
if (!this.conversationUrl) return;
|
||||||
|
try {
|
||||||
|
const url = `${window.chatwootConfig.hostURL}${this.conversationUrl}`;
|
||||||
|
await copyTextToClipboard(url);
|
||||||
|
useAlert(this.$t('CONVERSATION.CARD_CONTEXT_MENU.COPY_LINK_SUCCESS'));
|
||||||
|
this.$emit('close');
|
||||||
|
} catch (error) {
|
||||||
|
// error
|
||||||
|
}
|
||||||
|
},
|
||||||
show(key) {
|
show(key) {
|
||||||
// If the conversation status is same as the action, then don't display the option
|
// If the conversation status is same as the action, then don't display the option
|
||||||
// i.e.: Don't show an option to resolve if the conversation is already resolved.
|
// i.e.: Don't show an option to resolve if the conversation is already resolved.
|
||||||
@@ -217,83 +275,114 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="p-1 rounded-md shadow-xl bg-n-alpha-3/50 backdrop-blur-[100px]">
|
<div
|
||||||
<MenuItem
|
class="p-1 rounded-md shadow-xl bg-n-alpha-3/50 backdrop-blur-[100px] outline-1 outline outline-n-weak/50"
|
||||||
v-if="!hasUnreadMessages"
|
>
|
||||||
:option="unreadOption"
|
<template v-if="isAllowed([MENU.MARK_AS_READ, MENU.MARK_AS_UNREAD])">
|
||||||
variant="icon"
|
|
||||||
@click.stop="$emit('markAsUnread')"
|
|
||||||
/>
|
|
||||||
<MenuItem
|
|
||||||
v-else
|
|
||||||
:option="readOption"
|
|
||||||
variant="icon"
|
|
||||||
@click.stop="$emit('markAsRead')"
|
|
||||||
/>
|
|
||||||
<hr class="m-1 rounded border-b border-n-weak dark:border-n-weak" />
|
|
||||||
<template v-for="option in statusMenuConfig">
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
v-if="show(option.key)"
|
v-if="!hasUnreadMessages"
|
||||||
:key="option.key"
|
:option="unreadOption"
|
||||||
:option="option"
|
|
||||||
variant="icon"
|
variant="icon"
|
||||||
@click.stop="toggleStatus(option.key, null)"
|
@click.stop="$emit('markAsUnread')"
|
||||||
/>
|
/>
|
||||||
|
<MenuItem
|
||||||
|
v-else
|
||||||
|
:option="readOption"
|
||||||
|
variant="icon"
|
||||||
|
@click.stop="$emit('markAsRead')"
|
||||||
|
/>
|
||||||
|
<hr class="m-1 rounded border-b border-n-weak dark:border-n-weak" />
|
||||||
</template>
|
</template>
|
||||||
<MenuItem
|
<template v-if="isAllowed([MENU.STATUS, MENU.SNOOZE])">
|
||||||
v-if="showSnooze"
|
<template v-for="option in statusMenuConfig">
|
||||||
:option="snoozeOption"
|
|
||||||
variant="icon"
|
|
||||||
@click.stop="snoozeConversation()"
|
|
||||||
/>
|
|
||||||
<hr class="m-1 rounded border-b border-n-weak dark:border-n-weak" />
|
|
||||||
<MenuItemWithSubmenu :option="priorityConfig">
|
|
||||||
<MenuItem
|
|
||||||
v-for="(option, i) in priorityConfig.options"
|
|
||||||
:key="i"
|
|
||||||
:option="option"
|
|
||||||
@click.stop="assignPriority(option.key)"
|
|
||||||
/>
|
|
||||||
</MenuItemWithSubmenu>
|
|
||||||
<MenuItemWithSubmenu
|
|
||||||
:option="labelMenuConfig"
|
|
||||||
:sub-menu-available="!!labels.length"
|
|
||||||
>
|
|
||||||
<MenuItem
|
|
||||||
v-for="label in labels"
|
|
||||||
:key="label.id"
|
|
||||||
:option="generateMenuLabelConfig(label, 'label')"
|
|
||||||
variant="label"
|
|
||||||
@click.stop="$emit('assignLabel', label)"
|
|
||||||
/>
|
|
||||||
</MenuItemWithSubmenu>
|
|
||||||
<MenuItemWithSubmenu
|
|
||||||
:option="agentMenuConfig"
|
|
||||||
:sub-menu-available="!!assignableAgents.length"
|
|
||||||
>
|
|
||||||
<AgentLoadingPlaceholder v-if="assignableAgentsUiFlags.isFetching" />
|
|
||||||
<template v-else>
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
v-for="agent in assignableAgents"
|
v-if="show(option.key) && isAllowed([MENU.STATUS])"
|
||||||
:key="agent.id"
|
:key="option.key"
|
||||||
:option="generateMenuLabelConfig(agent, 'agent')"
|
:option="option"
|
||||||
variant="agent"
|
variant="icon"
|
||||||
@click.stop="$emit('assignAgent', agent)"
|
@click.stop="toggleStatus(option.key, null)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</MenuItemWithSubmenu>
|
|
||||||
<MenuItemWithSubmenu
|
|
||||||
:option="teamMenuConfig"
|
|
||||||
:sub-menu-available="!!teams.length"
|
|
||||||
>
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
v-for="team in teams"
|
v-if="showSnooze && isAllowed([MENU.SNOOZE])"
|
||||||
:key="team.id"
|
:option="snoozeOption"
|
||||||
:option="generateMenuLabelConfig(team, 'team')"
|
variant="icon"
|
||||||
@click.stop="$emit('assignTeam', team)"
|
@click.stop="snoozeConversation()"
|
||||||
/>
|
/>
|
||||||
</MenuItemWithSubmenu>
|
<hr class="m-1 rounded border-b border-n-weak dark:border-n-weak" />
|
||||||
<template v-if="isAdmin">
|
</template>
|
||||||
|
<template
|
||||||
|
v-if="isAllowed([MENU.PRIORITY, MENU.LABEL, MENU.AGENT, MENU.TEAM])"
|
||||||
|
>
|
||||||
|
<MenuItemWithSubmenu
|
||||||
|
v-if="isAllowed([MENU.PRIORITY])"
|
||||||
|
:option="priorityConfig"
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
v-for="(option, i) in priorityConfig.options"
|
||||||
|
:key="i"
|
||||||
|
:option="option"
|
||||||
|
@click.stop="assignPriority(option.key)"
|
||||||
|
/>
|
||||||
|
</MenuItemWithSubmenu>
|
||||||
|
<MenuItemWithSubmenu
|
||||||
|
v-if="isAllowed([MENU.LABEL])"
|
||||||
|
:option="labelMenuConfig"
|
||||||
|
:sub-menu-available="!!labels.length"
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
v-for="label in labels"
|
||||||
|
:key="label.id"
|
||||||
|
:option="generateMenuLabelConfig(label, 'label')"
|
||||||
|
variant="label"
|
||||||
|
@click.stop="$emit('assignLabel', label)"
|
||||||
|
/>
|
||||||
|
</MenuItemWithSubmenu>
|
||||||
|
<MenuItemWithSubmenu
|
||||||
|
v-if="isAllowed([MENU.AGENT])"
|
||||||
|
:option="agentMenuConfig"
|
||||||
|
:sub-menu-available="!!assignableAgents.length"
|
||||||
|
>
|
||||||
|
<AgentLoadingPlaceholder v-if="assignableAgentsUiFlags.isFetching" />
|
||||||
|
<template v-else>
|
||||||
|
<MenuItem
|
||||||
|
v-for="agent in assignableAgents"
|
||||||
|
:key="agent.id"
|
||||||
|
:option="generateMenuLabelConfig(agent, 'agent')"
|
||||||
|
variant="agent"
|
||||||
|
@click.stop="$emit('assignAgent', agent)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</MenuItemWithSubmenu>
|
||||||
|
<MenuItemWithSubmenu
|
||||||
|
v-if="isAllowed([MENU.TEAM])"
|
||||||
|
:option="teamMenuConfig"
|
||||||
|
:sub-menu-available="!!teams.length"
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
v-for="team in teams"
|
||||||
|
:key="team.id"
|
||||||
|
:option="generateMenuLabelConfig(team, 'team')"
|
||||||
|
@click.stop="$emit('assignTeam', team)"
|
||||||
|
/>
|
||||||
|
</MenuItemWithSubmenu>
|
||||||
|
<hr class="m-1 rounded border-b border-n-weak dark:border-n-weak" />
|
||||||
|
</template>
|
||||||
|
<template v-if="isAllowed([MENU.OPEN_NEW_TAB, MENU.COPY_LINK])">
|
||||||
|
<MenuItem
|
||||||
|
v-if="isAllowed([MENU.OPEN_NEW_TAB])"
|
||||||
|
:option="openInNewTabOption"
|
||||||
|
variant="icon"
|
||||||
|
@click.stop="openInNewTab"
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
v-if="isAllowed([MENU.COPY_LINK])"
|
||||||
|
:option="copyLinkOption"
|
||||||
|
variant="icon"
|
||||||
|
@click.stop="copyConversationLink"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-if="isAdmin && isAllowed([MENU.DELETE])">
|
||||||
<hr class="m-1 rounded border-b border-n-weak dark:border-n-weak" />
|
<hr class="m-1 rounded border-b border-n-weak dark:border-n-weak" />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
:option="deleteOption"
|
:option="deleteOption"
|
||||||
|
|||||||
@@ -1,20 +1,16 @@
|
|||||||
<script>
|
<script setup>
|
||||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
||||||
export default {
|
|
||||||
components: {
|
defineProps({
|
||||||
Thumbnail,
|
option: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
},
|
},
|
||||||
props: {
|
variant: {
|
||||||
option: {
|
type: String,
|
||||||
type: Object,
|
default: 'default',
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
variant: {
|
|
||||||
type: String,
|
|
||||||
default: 'default',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -30,12 +26,12 @@ export default {
|
|||||||
class="label-pill flex-shrink-0"
|
class="label-pill flex-shrink-0"
|
||||||
:style="{ backgroundColor: option.color }"
|
:style="{ backgroundColor: option.color }"
|
||||||
/>
|
/>
|
||||||
<Thumbnail
|
<Avatar
|
||||||
v-if="variant === 'agent'"
|
v-if="variant === 'agent'"
|
||||||
:username="option.label"
|
:name="option.label"
|
||||||
:src="option.thumbnail"
|
:src="option.thumbnail"
|
||||||
:status="option.status"
|
:status="option.status === 'online' ? option.status : null"
|
||||||
size="20px"
|
:size="20"
|
||||||
class="flex-shrink-0"
|
class="flex-shrink-0"
|
||||||
/>
|
/>
|
||||||
<p class="menu-label truncate min-w-0 flex-1">
|
<p class="menu-label truncate min-w-0 flex-1">
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useStore } from 'dashboard/composables/store.js';
|
import { useStore } from 'dashboard/composables/store.js';
|
||||||
import { useAccount } from 'dashboard/composables/useAccount';
|
import { useAccount } from 'dashboard/composables/useAccount';
|
||||||
|
import { useConfig } from 'dashboard/composables/useConfig';
|
||||||
import { useCamelCase } from 'dashboard/composables/useTransformKeys';
|
import { useCamelCase } from 'dashboard/composables/useTransformKeys';
|
||||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||||
|
|
||||||
export function useCaptain() {
|
export function useCaptain() {
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const { isCloudFeatureEnabled, currentAccount } = useAccount();
|
const { isCloudFeatureEnabled, currentAccount } = useAccount();
|
||||||
|
const { isEnterprise } = useConfig();
|
||||||
|
|
||||||
const captainEnabled = computed(() => {
|
const captainEnabled = computed(() => {
|
||||||
return isCloudFeatureEnabled(FEATURE_FLAGS.CAPTAIN);
|
return isCloudFeatureEnabled(FEATURE_FLAGS.CAPTAIN);
|
||||||
@@ -33,7 +35,9 @@ export function useCaptain() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const fetchLimits = () => {
|
const fetchLimits = () => {
|
||||||
store.dispatch('accounts/limits');
|
if (isEnterprise) {
|
||||||
|
store.dispatch('accounts/limits');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export default {
|
|||||||
HELP_CENTER_DOCS_URL:
|
HELP_CENTER_DOCS_URL:
|
||||||
'https://www.chatwoot.com/docs/product/others/help-center',
|
'https://www.chatwoot.com/docs/product/others/help-center',
|
||||||
TESTIMONIAL_URL: 'https://testimonials.cdn.chatwoot.com/content.json',
|
TESTIMONIAL_URL: 'https://testimonials.cdn.chatwoot.com/content.json',
|
||||||
SMALL_SCREEN_BREAKPOINT: 1200,
|
SMALL_SCREEN_BREAKPOINT: 768,
|
||||||
AVAILABILITY_STATUS_KEYS: ['online', 'busy', 'offline'],
|
AVAILABILITY_STATUS_KEYS: ['online', 'busy', 'offline'],
|
||||||
SNOOZE_OPTIONS: {
|
SNOOZE_OPTIONS: {
|
||||||
UNTIL_NEXT_REPLY: 'until_next_reply',
|
UNTIL_NEXT_REPLY: 'until_next_reply',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const CONVERSATION_EVENTS = Object.freeze({
|
|||||||
TRANSLATE_A_MESSAGE: 'Translated a message',
|
TRANSLATE_A_MESSAGE: 'Translated a message',
|
||||||
INSERTED_A_VARIABLE: 'Inserted a variable',
|
INSERTED_A_VARIABLE: 'Inserted a variable',
|
||||||
INSERTED_AN_EMOJI: 'Inserted an emoji',
|
INSERTED_AN_EMOJI: 'Inserted an emoji',
|
||||||
|
INSERTED_A_TOOL: 'Inserted a tool',
|
||||||
USED_MENTIONS: 'Used mentions',
|
USED_MENTIONS: 'Used mentions',
|
||||||
SEARCH_CONVERSATION: 'Searched conversations',
|
SEARCH_CONVERSATION: 'Searched conversations',
|
||||||
APPLY_FILTER: 'Applied filters in the conversation list',
|
APPLY_FILTER: 'Applied filters in the conversation list',
|
||||||
|
|||||||
@@ -319,6 +319,12 @@ const createNode = (editorView, nodeType, content) => {
|
|||||||
return state.schema.text(`{{${content}}}`);
|
return state.schema.text(`{{${content}}}`);
|
||||||
case 'emoji':
|
case 'emoji':
|
||||||
return state.schema.text(content);
|
return state.schema.text(content);
|
||||||
|
case 'tool': {
|
||||||
|
return state.schema.nodes.tools.create({
|
||||||
|
id: content.id,
|
||||||
|
name: content.title,
|
||||||
|
});
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -355,6 +361,11 @@ const nodeCreators = {
|
|||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
}),
|
}),
|
||||||
|
tool: (editorView, content, from, to) => ({
|
||||||
|
node: createNode(editorView, 'tool', content),
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -146,6 +146,7 @@
|
|||||||
"SEND_WEBHOOK_EVENT": "Send Webhook Event",
|
"SEND_WEBHOOK_EVENT": "Send Webhook Event",
|
||||||
"SEND_ATTACHMENT": "Send Attachment",
|
"SEND_ATTACHMENT": "Send Attachment",
|
||||||
"SEND_MESSAGE": "Send a Message",
|
"SEND_MESSAGE": "Send a Message",
|
||||||
|
"ADD_PRIVATE_NOTE": "Add a Private Note",
|
||||||
"CHANGE_PRIORITY": "Change Priority",
|
"CHANGE_PRIORITY": "Change Priority",
|
||||||
"ADD_SLA": "Add SLA",
|
"ADD_SLA": "Add SLA",
|
||||||
"OPEN_CONVERSATION": "Open conversation"
|
"OPEN_CONVERSATION": "Open conversation"
|
||||||
|
|||||||
@@ -137,6 +137,70 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"WHATSAPP": {
|
||||||
|
"HEADER_TITLE": "WhatsApp campaigns",
|
||||||
|
"NEW_CAMPAIGN": "Create campaign",
|
||||||
|
"EMPTY_STATE": {
|
||||||
|
"TITLE": "No WhatsApp campaigns are available",
|
||||||
|
"SUBTITLE": "Launch a WhatsApp campaign to reach your customers directly. Send offers or make announcements with ease. Click 'Create campaign' to get started."
|
||||||
|
},
|
||||||
|
"CARD": {
|
||||||
|
"STATUS": {
|
||||||
|
"COMPLETED": "Completed",
|
||||||
|
"SCHEDULED": "Scheduled"
|
||||||
|
},
|
||||||
|
"CAMPAIGN_DETAILS": {
|
||||||
|
"SENT_FROM": "Sent from",
|
||||||
|
"ON": "on"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CREATE": {
|
||||||
|
"TITLE": "Create WhatsApp campaign",
|
||||||
|
"CANCEL_BUTTON_TEXT": "Cancel",
|
||||||
|
"CREATE_BUTTON_TEXT": "Create",
|
||||||
|
"FORM": {
|
||||||
|
"TITLE": {
|
||||||
|
"LABEL": "Title",
|
||||||
|
"PLACEHOLDER": "Please enter the title of campaign",
|
||||||
|
"ERROR": "Title is required"
|
||||||
|
},
|
||||||
|
"INBOX": {
|
||||||
|
"LABEL": "Select Inbox",
|
||||||
|
"PLACEHOLDER": "Select Inbox",
|
||||||
|
"ERROR": "Inbox is required"
|
||||||
|
},
|
||||||
|
"TEMPLATE": {
|
||||||
|
"LABEL": "WhatsApp Template",
|
||||||
|
"PLACEHOLDER": "Select a template",
|
||||||
|
"INFO": "Select a template to use for this campaign.",
|
||||||
|
"ERROR": "Template is required",
|
||||||
|
"PREVIEW_TITLE": "Process {templateName}",
|
||||||
|
"LANGUAGE": "Language",
|
||||||
|
"CATEGORY": "Category",
|
||||||
|
"VARIABLES_LABEL": "Variables",
|
||||||
|
"VARIABLE_PLACEHOLDER": "Enter value for {variable}"
|
||||||
|
},
|
||||||
|
"AUDIENCE": {
|
||||||
|
"LABEL": "Audience",
|
||||||
|
"PLACEHOLDER": "Select the customer labels",
|
||||||
|
"ERROR": "Audience is required"
|
||||||
|
},
|
||||||
|
"SCHEDULED_AT": {
|
||||||
|
"LABEL": "Scheduled time",
|
||||||
|
"PLACEHOLDER": "Please select the time",
|
||||||
|
"ERROR": "Scheduled time is required"
|
||||||
|
},
|
||||||
|
"BUTTONS": {
|
||||||
|
"CREATE": "Create",
|
||||||
|
"CANCEL": "Cancel"
|
||||||
|
},
|
||||||
|
"API": {
|
||||||
|
"SUCCESS_MESSAGE": "WhatsApp campaign created successfully",
|
||||||
|
"ERROR_MESSAGE": "There was an error. Please try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"CONFIRM_DELETE": {
|
"CONFIRM_DELETE": {
|
||||||
"TITLE": "Are you sure to delete?",
|
"TITLE": "Are you sure to delete?",
|
||||||
"DESCRIPTION": "The delete action is permanent and cannot be reversed.",
|
"DESCRIPTION": "The delete action is permanent and cannot be reversed.",
|
||||||
|
|||||||
@@ -222,10 +222,17 @@
|
|||||||
"DESC": "Start supporting your customers via WhatsApp.",
|
"DESC": "Start supporting your customers via WhatsApp.",
|
||||||
"PROVIDERS": {
|
"PROVIDERS": {
|
||||||
"LABEL": "API Provider",
|
"LABEL": "API Provider",
|
||||||
|
"WHATSAPP_EMBEDDED": "WhatsApp Business",
|
||||||
"TWILIO": "Twilio",
|
"TWILIO": "Twilio",
|
||||||
"WHATSAPP_CLOUD": "WhatsApp Cloud",
|
"WHATSAPP_CLOUD": "WhatsApp Cloud",
|
||||||
|
"WHATSAPP_CLOUD_DESC": "Quick setup through Meta",
|
||||||
|
"TWILIO_DESC": "Connect via Twilio credentials",
|
||||||
"360_DIALOG": "360Dialog"
|
"360_DIALOG": "360Dialog"
|
||||||
},
|
},
|
||||||
|
"SELECT_PROVIDER": {
|
||||||
|
"TITLE": "Select your API provider",
|
||||||
|
"DESCRIPTION": "Choose your WhatsApp provider. You can connect directly through Meta which requires no setup, or connect through Twilio using your account credentials."
|
||||||
|
},
|
||||||
"INBOX_NAME": {
|
"INBOX_NAME": {
|
||||||
"LABEL": "Inbox Name",
|
"LABEL": "Inbox Name",
|
||||||
"PLACEHOLDER": "Please enter an inbox name",
|
"PLACEHOLDER": "Please enter an inbox name",
|
||||||
@@ -264,6 +271,28 @@
|
|||||||
"WEBHOOK_VERIFICATION_TOKEN": "Webhook Verification Token"
|
"WEBHOOK_VERIFICATION_TOKEN": "Webhook Verification Token"
|
||||||
},
|
},
|
||||||
"SUBMIT_BUTTON": "Create WhatsApp Channel",
|
"SUBMIT_BUTTON": "Create WhatsApp Channel",
|
||||||
|
"EMBEDDED_SIGNUP": {
|
||||||
|
"TITLE": "Quick Setup with Meta",
|
||||||
|
"DESC": "You will be redirected to Meta to log into your WhatsApp Business account. Having admin access will help make the setup smooth and easy.",
|
||||||
|
"BENEFITS": {
|
||||||
|
"TITLE": "Benefits of Embedded Signup:",
|
||||||
|
"EASY_SETUP": "No manual configuration required",
|
||||||
|
"SECURE_AUTH": "Secure OAuth based authentication",
|
||||||
|
"AUTO_CONFIG": "Automatic webhook and phone number configuration"
|
||||||
|
},
|
||||||
|
"SUBMIT_BUTTON": "Connect with WhatsApp Business",
|
||||||
|
"AUTH_PROCESSING": "Authenticating with Meta",
|
||||||
|
"WAITING_FOR_BUSINESS_INFO": "Please complete business setup in the Meta window...",
|
||||||
|
"PROCESSING": "Setting up your WhatsApp Business Account",
|
||||||
|
"LOADING_SDK": "Loading Facebook SDK...",
|
||||||
|
"CANCELLED": "WhatsApp Signup was cancelled",
|
||||||
|
"SUCCESS_TITLE": "WhatsApp Business Account Connected!",
|
||||||
|
"WAITING_FOR_AUTH": "Waiting for authentication...",
|
||||||
|
"INVALID_BUSINESS_DATA": "Invalid business data received from Facebook. Please try again.",
|
||||||
|
"SIGNUP_ERROR": "Signup error occurred",
|
||||||
|
"AUTH_NOT_COMPLETED": "Authentication not completed. Please restart the process.",
|
||||||
|
"SUCCESS_FALLBACK": "WhatsApp Business Account has been successfully configured"
|
||||||
|
},
|
||||||
"API": {
|
"API": {
|
||||||
"ERROR_MESSAGE": "We were not able to save the WhatsApp channel"
|
"ERROR_MESSAGE": "We were not able to save the WhatsApp channel"
|
||||||
}
|
}
|
||||||
@@ -571,6 +600,10 @@
|
|||||||
"WHATSAPP_SECTION_UPDATE_BUTTON": "Update",
|
"WHATSAPP_SECTION_UPDATE_BUTTON": "Update",
|
||||||
"WHATSAPP_WEBHOOK_TITLE": "Webhook Verification Token",
|
"WHATSAPP_WEBHOOK_TITLE": "Webhook Verification Token",
|
||||||
"WHATSAPP_WEBHOOK_SUBHEADER": "This token is used to verify the authenticity of the webhook endpoint.",
|
"WHATSAPP_WEBHOOK_SUBHEADER": "This token is used to verify the authenticity of the webhook endpoint.",
|
||||||
|
"WHATSAPP_TEMPLATES_SYNC_TITLE": "Sync Templates",
|
||||||
|
"WHATSAPP_TEMPLATES_SYNC_SUBHEADER": "Manually sync message templates from WhatsApp to update your available templates.",
|
||||||
|
"WHATSAPP_TEMPLATES_SYNC_BUTTON": "Sync Templates",
|
||||||
|
"WHATSAPP_TEMPLATES_SYNC_SUCCESS": "Templates sync initiated successfully. It may take a couple of minutes to update.",
|
||||||
"UPDATE_PRE_CHAT_FORM_SETTINGS": "Update Pre Chat Form Settings"
|
"UPDATE_PRE_CHAT_FORM_SETTINGS": "Update Pre Chat Form Settings"
|
||||||
},
|
},
|
||||||
"HELP_CENTER": {
|
"HELP_CENTER": {
|
||||||
|
|||||||
@@ -524,6 +524,100 @@
|
|||||||
"TITLE": "Captain Assistant",
|
"TITLE": "Captain Assistant",
|
||||||
"NOTE": "Captain Assistant engages directly with customers, learns from your help docs and past conversations, and delivers instant, accurate responses. It handles the initial queries, providing quick resolutions before transferring to an agent when needed."
|
"NOTE": "Captain Assistant engages directly with customers, learns from your help docs and past conversations, and delivers instant, accurate responses. It handles the initial queries, providing quick resolutions before transferring to an agent when needed."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"GUARDRAILS": {
|
||||||
|
"TITLE": "Guardrails",
|
||||||
|
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic.",
|
||||||
|
"BREADCRUMB": {
|
||||||
|
"TITLE": "Guardrails"
|
||||||
|
},
|
||||||
|
"BULK_ACTION": {
|
||||||
|
"SELECTED": "{count} item selected | {count} items selected",
|
||||||
|
"SELECT_ALL": "Select all ({count})",
|
||||||
|
"UNSELECT_ALL": "Unselect all ({count})",
|
||||||
|
"BULK_DELETE_BUTTON": "Delete"
|
||||||
|
},
|
||||||
|
"ADD": {
|
||||||
|
"SUGGESTED": {
|
||||||
|
"TITLE": "Example guardrails",
|
||||||
|
"ADD": "Add all",
|
||||||
|
"ADD_SINGLE": "Add this",
|
||||||
|
"SAVE": "Add and save (↵)",
|
||||||
|
"PLACEHOLDER": "Type in another guardrail..."
|
||||||
|
},
|
||||||
|
"NEW": {
|
||||||
|
"TITLE": "Add a guardrail",
|
||||||
|
"CREATE": "Create",
|
||||||
|
"CANCEL": "Cancel",
|
||||||
|
"PLACEHOLDER": "Type in another guardrail...",
|
||||||
|
"TEST_ALL": "Test all"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LIST": {
|
||||||
|
"SEARCH_PLACEHOLDER": "Search..."
|
||||||
|
},
|
||||||
|
"EMPTY_MESSAGE": "No guardrails found. Create or add examples to begin.",
|
||||||
|
"API": {
|
||||||
|
"ADD": {
|
||||||
|
"SUCCESS": "Guardrails added successfully",
|
||||||
|
"ERROR": "There was an error adding guardrails, please try again."
|
||||||
|
},
|
||||||
|
"UPDATE": {
|
||||||
|
"SUCCESS": "Guardrails updated successfully",
|
||||||
|
"ERROR": "There was an error updating guardrails, please try again."
|
||||||
|
},
|
||||||
|
"DELETE": {
|
||||||
|
"SUCCESS": "Guardrails deleted successfully",
|
||||||
|
"ERROR": "There was an error deleting guardrails, please try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RESPONSE_GUIDELINES": {
|
||||||
|
"TITLE": "Response Guidelines",
|
||||||
|
"DESCRIPTION": "The vibe and structure of your assistant’s replies—clear and friendly? Short and snappy? Detailed and formal?",
|
||||||
|
"BREADCRUMB": {
|
||||||
|
"TITLE": "Response Guidelines"
|
||||||
|
},
|
||||||
|
"BULK_ACTION": {
|
||||||
|
"SELECTED": "{count} item selected | {count} items selected",
|
||||||
|
"SELECT_ALL": "Select all ({count})",
|
||||||
|
"UNSELECT_ALL": "Unselect all ({count})",
|
||||||
|
"BULK_DELETE_BUTTON": "Delete"
|
||||||
|
},
|
||||||
|
"ADD": {
|
||||||
|
"SUGGESTED": {
|
||||||
|
"TITLE": "Example response guidelines",
|
||||||
|
"ADD": "Add all",
|
||||||
|
"ADD_SINGLE": "Add this",
|
||||||
|
"SAVE": "Add and save (↵)",
|
||||||
|
"PLACEHOLDER": "Type in another response guideline..."
|
||||||
|
},
|
||||||
|
"NEW": {
|
||||||
|
"TITLE": "Add a response guideline",
|
||||||
|
"CREATE": "Create",
|
||||||
|
"CANCEL": "Cancel",
|
||||||
|
"PLACEHOLDER": "Type in another response guideline...",
|
||||||
|
"TEST_ALL": "Test all"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LIST": {
|
||||||
|
"SEARCH_PLACEHOLDER": "Search..."
|
||||||
|
},
|
||||||
|
"EMPTY_MESSAGE": "No response guidelines found. Create or add examples to begin.",
|
||||||
|
"API": {
|
||||||
|
"ADD": {
|
||||||
|
"SUCCESS": "Response Guidelines added successfully",
|
||||||
|
"ERROR": "There was an error adding response guidelines, please try again."
|
||||||
|
},
|
||||||
|
"UPDATE": {
|
||||||
|
"SUCCESS": "Response Guidelines updated successfully",
|
||||||
|
"ERROR": "There was an error updating response guidelines, please try again."
|
||||||
|
},
|
||||||
|
"DELETE": {
|
||||||
|
"SUCCESS": "Response Guidelines deleted successfully",
|
||||||
|
"ERROR": "There was an error deleting response guidelines, please try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"DOCUMENTS": {
|
"DOCUMENTS": {
|
||||||
|
|||||||
@@ -319,6 +319,7 @@
|
|||||||
"CSAT": "CSAT",
|
"CSAT": "CSAT",
|
||||||
"LIVE_CHAT": "Live Chat",
|
"LIVE_CHAT": "Live Chat",
|
||||||
"SMS": "SMS",
|
"SMS": "SMS",
|
||||||
|
"WHATSAPP": "WhatsApp",
|
||||||
"CAMPAIGNS": "Campaigns",
|
"CAMPAIGNS": "Campaigns",
|
||||||
"ONGOING": "Ongoing",
|
"ONGOING": "Ongoing",
|
||||||
"ONE_OFF": "One off",
|
"ONE_OFF": "One off",
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
"PICKER": {
|
"PICKER": {
|
||||||
"SEARCH_PLACEHOLDER": "Search Templates",
|
"SEARCH_PLACEHOLDER": "Search Templates",
|
||||||
"NO_TEMPLATES_FOUND": "No templates found for",
|
"NO_TEMPLATES_FOUND": "No templates found for",
|
||||||
|
"NO_TEMPLATES_AVAILABLE": "No WhatsApp templates available. Click refresh to sync templates from WhatsApp.",
|
||||||
|
"REFRESH_BUTTON": "Refresh templates",
|
||||||
|
"REFRESH_SUCCESS": "Templates refresh initiated. It may take a couple of minutes to update.",
|
||||||
|
"REFRESH_ERROR": "Failed to refresh templates. Please try again.",
|
||||||
"LABELS": {
|
"LABELS": {
|
||||||
"LANGUAGE": "Language",
|
"LANGUAGE": "Language",
|
||||||
"TEMPLATE_BODY": "Template Body",
|
"TEMPLATE_BODY": "Template Body",
|
||||||
|
|||||||
@@ -146,6 +146,7 @@
|
|||||||
"SEND_WEBHOOK_EVENT": "Send Webhook Event",
|
"SEND_WEBHOOK_EVENT": "Send Webhook Event",
|
||||||
"SEND_ATTACHMENT": "Send Attachment",
|
"SEND_ATTACHMENT": "Send Attachment",
|
||||||
"SEND_MESSAGE": "Send a Message",
|
"SEND_MESSAGE": "Send a Message",
|
||||||
|
"ADD_PRIVATE_NOTE": "Add a Private Note",
|
||||||
"CHANGE_PRIORITY": "تغيير الأولوية",
|
"CHANGE_PRIORITY": "تغيير الأولوية",
|
||||||
"ADD_SLA": "Add SLA",
|
"ADD_SLA": "Add SLA",
|
||||||
"OPEN_CONVERSATION": "فتح المحادثة"
|
"OPEN_CONVERSATION": "فتح المحادثة"
|
||||||
|
|||||||
@@ -137,6 +137,70 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"WHATSAPP": {
|
||||||
|
"HEADER_TITLE": "WhatsApp campaigns",
|
||||||
|
"NEW_CAMPAIGN": "Create campaign",
|
||||||
|
"EMPTY_STATE": {
|
||||||
|
"TITLE": "No WhatsApp campaigns are available",
|
||||||
|
"SUBTITLE": "Launch a WhatsApp campaign to reach your customers directly. Send offers or make announcements with ease. Click 'Create campaign' to get started."
|
||||||
|
},
|
||||||
|
"CARD": {
|
||||||
|
"STATUS": {
|
||||||
|
"COMPLETED": "مكتمل",
|
||||||
|
"SCHEDULED": "Scheduled"
|
||||||
|
},
|
||||||
|
"CAMPAIGN_DETAILS": {
|
||||||
|
"SENT_FROM": "Sent from",
|
||||||
|
"ON": "on"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CREATE": {
|
||||||
|
"TITLE": "Create WhatsApp campaign",
|
||||||
|
"CANCEL_BUTTON_TEXT": "إلغاء",
|
||||||
|
"CREATE_BUTTON_TEXT": "إنشاء",
|
||||||
|
"FORM": {
|
||||||
|
"TITLE": {
|
||||||
|
"LABEL": "العنوان",
|
||||||
|
"PLACEHOLDER": "الرجاء إدخال عنوان الحملة",
|
||||||
|
"ERROR": "العنوان مطلوب"
|
||||||
|
},
|
||||||
|
"INBOX": {
|
||||||
|
"LABEL": "اختر صندوق الوارد",
|
||||||
|
"PLACEHOLDER": "اختر صندوق الوارد",
|
||||||
|
"ERROR": "صندوق الوارد مطلوب"
|
||||||
|
},
|
||||||
|
"TEMPLATE": {
|
||||||
|
"LABEL": "WhatsApp Template",
|
||||||
|
"PLACEHOLDER": "Select a template",
|
||||||
|
"INFO": "Select a template to use for this campaign.",
|
||||||
|
"ERROR": "Template is required",
|
||||||
|
"PREVIEW_TITLE": "معالجة {templateName}",
|
||||||
|
"LANGUAGE": "اللغة",
|
||||||
|
"CATEGORY": "الفئة",
|
||||||
|
"VARIABLES_LABEL": "المتغيرات",
|
||||||
|
"VARIABLE_PLACEHOLDER": "Enter value for {variable}"
|
||||||
|
},
|
||||||
|
"AUDIENCE": {
|
||||||
|
"LABEL": "الجمهور",
|
||||||
|
"PLACEHOLDER": "حدد أوسمة العملاء",
|
||||||
|
"ERROR": "الجمهور مطلوب"
|
||||||
|
},
|
||||||
|
"SCHEDULED_AT": {
|
||||||
|
"LABEL": "الوقت المجدول",
|
||||||
|
"PLACEHOLDER": "الرجاء اختيار الوقت",
|
||||||
|
"ERROR": "الوقت المجدول مطلوب"
|
||||||
|
},
|
||||||
|
"BUTTONS": {
|
||||||
|
"CREATE": "إنشاء",
|
||||||
|
"CANCEL": "إلغاء"
|
||||||
|
},
|
||||||
|
"API": {
|
||||||
|
"SUCCESS_MESSAGE": "WhatsApp campaign created successfully",
|
||||||
|
"ERROR_MESSAGE": "حدث خطأ، الرجاء المحاولة مرة أخرى."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"CONFIRM_DELETE": {
|
"CONFIRM_DELETE": {
|
||||||
"TITLE": "هل أنت متأكد من الحذف?",
|
"TITLE": "هل أنت متأكد من الحذف?",
|
||||||
"DESCRIPTION": "The delete action is permanent and cannot be reversed.",
|
"DESCRIPTION": "The delete action is permanent and cannot be reversed.",
|
||||||
|
|||||||
@@ -222,10 +222,17 @@
|
|||||||
"DESC": "ابدأ في دعم عملائك عبر واتس آب.",
|
"DESC": "ابدأ في دعم عملائك عبر واتس آب.",
|
||||||
"PROVIDERS": {
|
"PROVIDERS": {
|
||||||
"LABEL": "API Provider",
|
"LABEL": "API Provider",
|
||||||
|
"WHATSAPP_EMBEDDED": "WhatsApp Business",
|
||||||
"TWILIO": "تويليو",
|
"TWILIO": "تويليو",
|
||||||
"WHATSAPP_CLOUD": "واتساب السحابة",
|
"WHATSAPP_CLOUD": "واتساب السحابة",
|
||||||
|
"WHATSAPP_CLOUD_DESC": "Quick setup through Meta",
|
||||||
|
"TWILIO_DESC": "Connect via Twilio credentials",
|
||||||
"360_DIALOG": "360dialog"
|
"360_DIALOG": "360dialog"
|
||||||
},
|
},
|
||||||
|
"SELECT_PROVIDER": {
|
||||||
|
"TITLE": "Select your API provider",
|
||||||
|
"DESCRIPTION": "Choose your WhatsApp provider. You can connect directly through Meta which requires no setup, or connect through Twilio using your account credentials."
|
||||||
|
},
|
||||||
"INBOX_NAME": {
|
"INBOX_NAME": {
|
||||||
"LABEL": "اسم صندوق الوارد لقناة التواصل",
|
"LABEL": "اسم صندوق الوارد لقناة التواصل",
|
||||||
"PLACEHOLDER": "الرجاء إدخال اسم القناة",
|
"PLACEHOLDER": "الرجاء إدخال اسم القناة",
|
||||||
@@ -264,6 +271,28 @@
|
|||||||
"WEBHOOK_VERIFICATION_TOKEN": "رمز التحقق من Webhook"
|
"WEBHOOK_VERIFICATION_TOKEN": "رمز التحقق من Webhook"
|
||||||
},
|
},
|
||||||
"SUBMIT_BUTTON": "إنشاء قناة واتساب",
|
"SUBMIT_BUTTON": "إنشاء قناة واتساب",
|
||||||
|
"EMBEDDED_SIGNUP": {
|
||||||
|
"TITLE": "Quick Setup with Meta",
|
||||||
|
"DESC": "You will be redirected to Meta to log into your WhatsApp Business account. Having admin access will help make the setup smooth and easy.",
|
||||||
|
"BENEFITS": {
|
||||||
|
"TITLE": "Benefits of Embedded Signup:",
|
||||||
|
"EASY_SETUP": "No manual configuration required",
|
||||||
|
"SECURE_AUTH": "Secure OAuth based authentication",
|
||||||
|
"AUTO_CONFIG": "Automatic webhook and phone number configuration"
|
||||||
|
},
|
||||||
|
"SUBMIT_BUTTON": "Connect with WhatsApp Business",
|
||||||
|
"AUTH_PROCESSING": "Authenticating with Meta",
|
||||||
|
"WAITING_FOR_BUSINESS_INFO": "Please complete business setup in the Meta window...",
|
||||||
|
"PROCESSING": "Setting up your WhatsApp Business Account",
|
||||||
|
"LOADING_SDK": "Loading Facebook SDK...",
|
||||||
|
"CANCELLED": "WhatsApp Signup was cancelled",
|
||||||
|
"SUCCESS_TITLE": "WhatsApp Business Account Connected!",
|
||||||
|
"WAITING_FOR_AUTH": "Waiting for authentication...",
|
||||||
|
"INVALID_BUSINESS_DATA": "Invalid business data received from Facebook. Please try again.",
|
||||||
|
"SIGNUP_ERROR": "Signup error occurred",
|
||||||
|
"AUTH_NOT_COMPLETED": "Authentication not completed. Please restart the process.",
|
||||||
|
"SUCCESS_FALLBACK": "WhatsApp Business Account has been successfully configured"
|
||||||
|
},
|
||||||
"API": {
|
"API": {
|
||||||
"ERROR_MESSAGE": "لم نتمكن من حفظ قناة واتساب"
|
"ERROR_MESSAGE": "لم نتمكن من حفظ قناة واتساب"
|
||||||
}
|
}
|
||||||
@@ -571,6 +600,10 @@
|
|||||||
"WHATSAPP_SECTION_UPDATE_BUTTON": "تحديث",
|
"WHATSAPP_SECTION_UPDATE_BUTTON": "تحديث",
|
||||||
"WHATSAPP_WEBHOOK_TITLE": "رمز التحقق من Webhook",
|
"WHATSAPP_WEBHOOK_TITLE": "رمز التحقق من Webhook",
|
||||||
"WHATSAPP_WEBHOOK_SUBHEADER": "This token is used to verify the authenticity of the webhook endpoint.",
|
"WHATSAPP_WEBHOOK_SUBHEADER": "This token is used to verify the authenticity of the webhook endpoint.",
|
||||||
|
"WHATSAPP_TEMPLATES_SYNC_TITLE": "Sync Templates",
|
||||||
|
"WHATSAPP_TEMPLATES_SYNC_SUBHEADER": "Manually sync message templates from WhatsApp to update your available templates.",
|
||||||
|
"WHATSAPP_TEMPLATES_SYNC_BUTTON": "Sync Templates",
|
||||||
|
"WHATSAPP_TEMPLATES_SYNC_SUCCESS": "Templates sync initiated successfully. It may take a couple of minutes to update.",
|
||||||
"UPDATE_PRE_CHAT_FORM_SETTINGS": "Update Pre Chat Form Settings"
|
"UPDATE_PRE_CHAT_FORM_SETTINGS": "Update Pre Chat Form Settings"
|
||||||
},
|
},
|
||||||
"HELP_CENTER": {
|
"HELP_CENTER": {
|
||||||
|
|||||||
@@ -524,6 +524,100 @@
|
|||||||
"TITLE": "Captain Assistant",
|
"TITLE": "Captain Assistant",
|
||||||
"NOTE": "Captain Assistant engages directly with customers, learns from your help docs and past conversations, and delivers instant, accurate responses. It handles the initial queries, providing quick resolutions before transferring to an agent when needed."
|
"NOTE": "Captain Assistant engages directly with customers, learns from your help docs and past conversations, and delivers instant, accurate responses. It handles the initial queries, providing quick resolutions before transferring to an agent when needed."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"GUARDRAILS": {
|
||||||
|
"TITLE": "Guardrails",
|
||||||
|
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic.",
|
||||||
|
"BREADCRUMB": {
|
||||||
|
"TITLE": "Guardrails"
|
||||||
|
},
|
||||||
|
"BULK_ACTION": {
|
||||||
|
"SELECTED": "{count} item selected | {count} items selected",
|
||||||
|
"SELECT_ALL": "Select all ({count})",
|
||||||
|
"UNSELECT_ALL": "Unselect all ({count})",
|
||||||
|
"BULK_DELETE_BUTTON": "حذف"
|
||||||
|
},
|
||||||
|
"ADD": {
|
||||||
|
"SUGGESTED": {
|
||||||
|
"TITLE": "Example guardrails",
|
||||||
|
"ADD": "Add all",
|
||||||
|
"ADD_SINGLE": "Add this",
|
||||||
|
"SAVE": "Add and save (↵)",
|
||||||
|
"PLACEHOLDER": "Type in another guardrail..."
|
||||||
|
},
|
||||||
|
"NEW": {
|
||||||
|
"TITLE": "Add a guardrail",
|
||||||
|
"CREATE": "إنشاء",
|
||||||
|
"CANCEL": "إلغاء",
|
||||||
|
"PLACEHOLDER": "Type in another guardrail...",
|
||||||
|
"TEST_ALL": "Test all"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LIST": {
|
||||||
|
"SEARCH_PLACEHOLDER": "Search..."
|
||||||
|
},
|
||||||
|
"EMPTY_MESSAGE": "No guardrails found. Create or add examples to begin.",
|
||||||
|
"API": {
|
||||||
|
"ADD": {
|
||||||
|
"SUCCESS": "Guardrails added successfully",
|
||||||
|
"ERROR": "There was an error adding guardrails, please try again."
|
||||||
|
},
|
||||||
|
"UPDATE": {
|
||||||
|
"SUCCESS": "Guardrails updated successfully",
|
||||||
|
"ERROR": "There was an error updating guardrails, please try again."
|
||||||
|
},
|
||||||
|
"DELETE": {
|
||||||
|
"SUCCESS": "Guardrails deleted successfully",
|
||||||
|
"ERROR": "There was an error deleting guardrails, please try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RESPONSE_GUIDELINES": {
|
||||||
|
"TITLE": "Response Guidelines",
|
||||||
|
"DESCRIPTION": "The vibe and structure of your assistant’s replies—clear and friendly? Short and snappy? Detailed and formal?",
|
||||||
|
"BREADCRUMB": {
|
||||||
|
"TITLE": "Response Guidelines"
|
||||||
|
},
|
||||||
|
"BULK_ACTION": {
|
||||||
|
"SELECTED": "{count} item selected | {count} items selected",
|
||||||
|
"SELECT_ALL": "Select all ({count})",
|
||||||
|
"UNSELECT_ALL": "Unselect all ({count})",
|
||||||
|
"BULK_DELETE_BUTTON": "حذف"
|
||||||
|
},
|
||||||
|
"ADD": {
|
||||||
|
"SUGGESTED": {
|
||||||
|
"TITLE": "Example response guidelines",
|
||||||
|
"ADD": "Add all",
|
||||||
|
"ADD_SINGLE": "Add this",
|
||||||
|
"SAVE": "Add and save (↵)",
|
||||||
|
"PLACEHOLDER": "Type in another response guideline..."
|
||||||
|
},
|
||||||
|
"NEW": {
|
||||||
|
"TITLE": "Add a response guideline",
|
||||||
|
"CREATE": "إنشاء",
|
||||||
|
"CANCEL": "إلغاء",
|
||||||
|
"PLACEHOLDER": "Type in another response guideline...",
|
||||||
|
"TEST_ALL": "Test all"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LIST": {
|
||||||
|
"SEARCH_PLACEHOLDER": "Search..."
|
||||||
|
},
|
||||||
|
"EMPTY_MESSAGE": "No response guidelines found. Create or add examples to begin.",
|
||||||
|
"API": {
|
||||||
|
"ADD": {
|
||||||
|
"SUCCESS": "Response Guidelines added successfully",
|
||||||
|
"ERROR": "There was an error adding response guidelines, please try again."
|
||||||
|
},
|
||||||
|
"UPDATE": {
|
||||||
|
"SUCCESS": "Response Guidelines updated successfully",
|
||||||
|
"ERROR": "There was an error updating response guidelines, please try again."
|
||||||
|
},
|
||||||
|
"DELETE": {
|
||||||
|
"SUCCESS": "Response Guidelines deleted successfully",
|
||||||
|
"ERROR": "There was an error deleting response guidelines, please try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"DOCUMENTS": {
|
"DOCUMENTS": {
|
||||||
|
|||||||
@@ -319,6 +319,7 @@
|
|||||||
"CSAT": "تقييم رضاء العملاء",
|
"CSAT": "تقييم رضاء العملاء",
|
||||||
"LIVE_CHAT": "Live Chat",
|
"LIVE_CHAT": "Live Chat",
|
||||||
"SMS": "SMS",
|
"SMS": "SMS",
|
||||||
|
"WHATSAPP": "واتساب",
|
||||||
"CAMPAIGNS": "الحملات",
|
"CAMPAIGNS": "الحملات",
|
||||||
"ONGOING": "جارية",
|
"ONGOING": "جارية",
|
||||||
"ONE_OFF": "إيقاف واحد",
|
"ONE_OFF": "إيقاف واحد",
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
"PICKER": {
|
"PICKER": {
|
||||||
"SEARCH_PLACEHOLDER": "نماذج البحث",
|
"SEARCH_PLACEHOLDER": "نماذج البحث",
|
||||||
"NO_TEMPLATES_FOUND": "لم يتم العثور على قوالب",
|
"NO_TEMPLATES_FOUND": "لم يتم العثور على قوالب",
|
||||||
|
"NO_TEMPLATES_AVAILABLE": "No WhatsApp templates available. Click refresh to sync templates from WhatsApp.",
|
||||||
|
"REFRESH_BUTTON": "Refresh templates",
|
||||||
|
"REFRESH_SUCCESS": "Templates refresh initiated. It may take a couple of minutes to update.",
|
||||||
|
"REFRESH_ERROR": "Failed to refresh templates. Please try again.",
|
||||||
"LABELS": {
|
"LABELS": {
|
||||||
"LANGUAGE": "اللغة",
|
"LANGUAGE": "اللغة",
|
||||||
"TEMPLATE_BODY": "نص القالب",
|
"TEMPLATE_BODY": "نص القالب",
|
||||||
|
|||||||
@@ -146,6 +146,7 @@
|
|||||||
"SEND_WEBHOOK_EVENT": "Send Webhook Event",
|
"SEND_WEBHOOK_EVENT": "Send Webhook Event",
|
||||||
"SEND_ATTACHMENT": "Send Attachment",
|
"SEND_ATTACHMENT": "Send Attachment",
|
||||||
"SEND_MESSAGE": "Send a Message",
|
"SEND_MESSAGE": "Send a Message",
|
||||||
|
"ADD_PRIVATE_NOTE": "Add a Private Note",
|
||||||
"CHANGE_PRIORITY": "Change Priority",
|
"CHANGE_PRIORITY": "Change Priority",
|
||||||
"ADD_SLA": "Add SLA",
|
"ADD_SLA": "Add SLA",
|
||||||
"OPEN_CONVERSATION": "Open conversation"
|
"OPEN_CONVERSATION": "Open conversation"
|
||||||
|
|||||||
@@ -137,6 +137,70 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"WHATSAPP": {
|
||||||
|
"HEADER_TITLE": "WhatsApp campaigns",
|
||||||
|
"NEW_CAMPAIGN": "Create campaign",
|
||||||
|
"EMPTY_STATE": {
|
||||||
|
"TITLE": "No WhatsApp campaigns are available",
|
||||||
|
"SUBTITLE": "Launch a WhatsApp campaign to reach your customers directly. Send offers or make announcements with ease. Click 'Create campaign' to get started."
|
||||||
|
},
|
||||||
|
"CARD": {
|
||||||
|
"STATUS": {
|
||||||
|
"COMPLETED": "Completed",
|
||||||
|
"SCHEDULED": "Scheduled"
|
||||||
|
},
|
||||||
|
"CAMPAIGN_DETAILS": {
|
||||||
|
"SENT_FROM": "Sent from",
|
||||||
|
"ON": "on"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CREATE": {
|
||||||
|
"TITLE": "Create WhatsApp campaign",
|
||||||
|
"CANCEL_BUTTON_TEXT": "Cancel",
|
||||||
|
"CREATE_BUTTON_TEXT": "Create",
|
||||||
|
"FORM": {
|
||||||
|
"TITLE": {
|
||||||
|
"LABEL": "Title",
|
||||||
|
"PLACEHOLDER": "Please enter the title of campaign",
|
||||||
|
"ERROR": "Title is required"
|
||||||
|
},
|
||||||
|
"INBOX": {
|
||||||
|
"LABEL": "Select Inbox",
|
||||||
|
"PLACEHOLDER": "Select Inbox",
|
||||||
|
"ERROR": "Inbox is required"
|
||||||
|
},
|
||||||
|
"TEMPLATE": {
|
||||||
|
"LABEL": "WhatsApp Template",
|
||||||
|
"PLACEHOLDER": "Select a template",
|
||||||
|
"INFO": "Select a template to use for this campaign.",
|
||||||
|
"ERROR": "Template is required",
|
||||||
|
"PREVIEW_TITLE": "Process {templateName}",
|
||||||
|
"LANGUAGE": "Language",
|
||||||
|
"CATEGORY": "Category",
|
||||||
|
"VARIABLES_LABEL": "Variables",
|
||||||
|
"VARIABLE_PLACEHOLDER": "Enter value for {variable}"
|
||||||
|
},
|
||||||
|
"AUDIENCE": {
|
||||||
|
"LABEL": "Audience",
|
||||||
|
"PLACEHOLDER": "Select the customer labels",
|
||||||
|
"ERROR": "Audience is required"
|
||||||
|
},
|
||||||
|
"SCHEDULED_AT": {
|
||||||
|
"LABEL": "Scheduled time",
|
||||||
|
"PLACEHOLDER": "Please select the time",
|
||||||
|
"ERROR": "Scheduled time is required"
|
||||||
|
},
|
||||||
|
"BUTTONS": {
|
||||||
|
"CREATE": "Create",
|
||||||
|
"CANCEL": "Cancel"
|
||||||
|
},
|
||||||
|
"API": {
|
||||||
|
"SUCCESS_MESSAGE": "WhatsApp campaign created successfully",
|
||||||
|
"ERROR_MESSAGE": "There was an error. Please try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"CONFIRM_DELETE": {
|
"CONFIRM_DELETE": {
|
||||||
"TITLE": "Are you sure to delete?",
|
"TITLE": "Are you sure to delete?",
|
||||||
"DESCRIPTION": "The delete action is permanent and cannot be reversed.",
|
"DESCRIPTION": "The delete action is permanent and cannot be reversed.",
|
||||||
|
|||||||
@@ -222,10 +222,17 @@
|
|||||||
"DESC": "Start supporting your customers via WhatsApp.",
|
"DESC": "Start supporting your customers via WhatsApp.",
|
||||||
"PROVIDERS": {
|
"PROVIDERS": {
|
||||||
"LABEL": "API Provider",
|
"LABEL": "API Provider",
|
||||||
|
"WHATSAPP_EMBEDDED": "WhatsApp Business",
|
||||||
"TWILIO": "Twilio",
|
"TWILIO": "Twilio",
|
||||||
"WHATSAPP_CLOUD": "WhatsApp Cloud",
|
"WHATSAPP_CLOUD": "WhatsApp Cloud",
|
||||||
|
"WHATSAPP_CLOUD_DESC": "Quick setup through Meta",
|
||||||
|
"TWILIO_DESC": "Connect via Twilio credentials",
|
||||||
"360_DIALOG": "360Dialog"
|
"360_DIALOG": "360Dialog"
|
||||||
},
|
},
|
||||||
|
"SELECT_PROVIDER": {
|
||||||
|
"TITLE": "Select your API provider",
|
||||||
|
"DESCRIPTION": "Choose your WhatsApp provider. You can connect directly through Meta which requires no setup, or connect through Twilio using your account credentials."
|
||||||
|
},
|
||||||
"INBOX_NAME": {
|
"INBOX_NAME": {
|
||||||
"LABEL": "Inbox Name",
|
"LABEL": "Inbox Name",
|
||||||
"PLACEHOLDER": "Please enter an inbox name",
|
"PLACEHOLDER": "Please enter an inbox name",
|
||||||
@@ -264,6 +271,28 @@
|
|||||||
"WEBHOOK_VERIFICATION_TOKEN": "Webhook Verification Token"
|
"WEBHOOK_VERIFICATION_TOKEN": "Webhook Verification Token"
|
||||||
},
|
},
|
||||||
"SUBMIT_BUTTON": "Create WhatsApp Channel",
|
"SUBMIT_BUTTON": "Create WhatsApp Channel",
|
||||||
|
"EMBEDDED_SIGNUP": {
|
||||||
|
"TITLE": "Quick Setup with Meta",
|
||||||
|
"DESC": "You will be redirected to Meta to log into your WhatsApp Business account. Having admin access will help make the setup smooth and easy.",
|
||||||
|
"BENEFITS": {
|
||||||
|
"TITLE": "Benefits of Embedded Signup:",
|
||||||
|
"EASY_SETUP": "No manual configuration required",
|
||||||
|
"SECURE_AUTH": "Secure OAuth based authentication",
|
||||||
|
"AUTO_CONFIG": "Automatic webhook and phone number configuration"
|
||||||
|
},
|
||||||
|
"SUBMIT_BUTTON": "Connect with WhatsApp Business",
|
||||||
|
"AUTH_PROCESSING": "Authenticating with Meta",
|
||||||
|
"WAITING_FOR_BUSINESS_INFO": "Please complete business setup in the Meta window...",
|
||||||
|
"PROCESSING": "Setting up your WhatsApp Business Account",
|
||||||
|
"LOADING_SDK": "Loading Facebook SDK...",
|
||||||
|
"CANCELLED": "WhatsApp Signup was cancelled",
|
||||||
|
"SUCCESS_TITLE": "WhatsApp Business Account Connected!",
|
||||||
|
"WAITING_FOR_AUTH": "Waiting for authentication...",
|
||||||
|
"INVALID_BUSINESS_DATA": "Invalid business data received from Facebook. Please try again.",
|
||||||
|
"SIGNUP_ERROR": "Signup error occurred",
|
||||||
|
"AUTH_NOT_COMPLETED": "Authentication not completed. Please restart the process.",
|
||||||
|
"SUCCESS_FALLBACK": "WhatsApp Business Account has been successfully configured"
|
||||||
|
},
|
||||||
"API": {
|
"API": {
|
||||||
"ERROR_MESSAGE": "We were not able to save the WhatsApp channel"
|
"ERROR_MESSAGE": "We were not able to save the WhatsApp channel"
|
||||||
}
|
}
|
||||||
@@ -571,6 +600,10 @@
|
|||||||
"WHATSAPP_SECTION_UPDATE_BUTTON": "Update",
|
"WHATSAPP_SECTION_UPDATE_BUTTON": "Update",
|
||||||
"WHATSAPP_WEBHOOK_TITLE": "Webhook Verification Token",
|
"WHATSAPP_WEBHOOK_TITLE": "Webhook Verification Token",
|
||||||
"WHATSAPP_WEBHOOK_SUBHEADER": "This token is used to verify the authenticity of the webhook endpoint.",
|
"WHATSAPP_WEBHOOK_SUBHEADER": "This token is used to verify the authenticity of the webhook endpoint.",
|
||||||
|
"WHATSAPP_TEMPLATES_SYNC_TITLE": "Sync Templates",
|
||||||
|
"WHATSAPP_TEMPLATES_SYNC_SUBHEADER": "Manually sync message templates from WhatsApp to update your available templates.",
|
||||||
|
"WHATSAPP_TEMPLATES_SYNC_BUTTON": "Sync Templates",
|
||||||
|
"WHATSAPP_TEMPLATES_SYNC_SUCCESS": "Templates sync initiated successfully. It may take a couple of minutes to update.",
|
||||||
"UPDATE_PRE_CHAT_FORM_SETTINGS": "Update Pre Chat Form Settings"
|
"UPDATE_PRE_CHAT_FORM_SETTINGS": "Update Pre Chat Form Settings"
|
||||||
},
|
},
|
||||||
"HELP_CENTER": {
|
"HELP_CENTER": {
|
||||||
|
|||||||
@@ -524,6 +524,100 @@
|
|||||||
"TITLE": "Captain Assistant",
|
"TITLE": "Captain Assistant",
|
||||||
"NOTE": "Captain Assistant engages directly with customers, learns from your help docs and past conversations, and delivers instant, accurate responses. It handles the initial queries, providing quick resolutions before transferring to an agent when needed."
|
"NOTE": "Captain Assistant engages directly with customers, learns from your help docs and past conversations, and delivers instant, accurate responses. It handles the initial queries, providing quick resolutions before transferring to an agent when needed."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"GUARDRAILS": {
|
||||||
|
"TITLE": "Guardrails",
|
||||||
|
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic.",
|
||||||
|
"BREADCRUMB": {
|
||||||
|
"TITLE": "Guardrails"
|
||||||
|
},
|
||||||
|
"BULK_ACTION": {
|
||||||
|
"SELECTED": "{count} item selected | {count} items selected",
|
||||||
|
"SELECT_ALL": "Select all ({count})",
|
||||||
|
"UNSELECT_ALL": "Unselect all ({count})",
|
||||||
|
"BULK_DELETE_BUTTON": "Delete"
|
||||||
|
},
|
||||||
|
"ADD": {
|
||||||
|
"SUGGESTED": {
|
||||||
|
"TITLE": "Example guardrails",
|
||||||
|
"ADD": "Add all",
|
||||||
|
"ADD_SINGLE": "Add this",
|
||||||
|
"SAVE": "Add and save (↵)",
|
||||||
|
"PLACEHOLDER": "Type in another guardrail..."
|
||||||
|
},
|
||||||
|
"NEW": {
|
||||||
|
"TITLE": "Add a guardrail",
|
||||||
|
"CREATE": "Create",
|
||||||
|
"CANCEL": "Cancel",
|
||||||
|
"PLACEHOLDER": "Type in another guardrail...",
|
||||||
|
"TEST_ALL": "Test all"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LIST": {
|
||||||
|
"SEARCH_PLACEHOLDER": "Search..."
|
||||||
|
},
|
||||||
|
"EMPTY_MESSAGE": "No guardrails found. Create or add examples to begin.",
|
||||||
|
"API": {
|
||||||
|
"ADD": {
|
||||||
|
"SUCCESS": "Guardrails added successfully",
|
||||||
|
"ERROR": "There was an error adding guardrails, please try again."
|
||||||
|
},
|
||||||
|
"UPDATE": {
|
||||||
|
"SUCCESS": "Guardrails updated successfully",
|
||||||
|
"ERROR": "There was an error updating guardrails, please try again."
|
||||||
|
},
|
||||||
|
"DELETE": {
|
||||||
|
"SUCCESS": "Guardrails deleted successfully",
|
||||||
|
"ERROR": "There was an error deleting guardrails, please try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RESPONSE_GUIDELINES": {
|
||||||
|
"TITLE": "Response Guidelines",
|
||||||
|
"DESCRIPTION": "The vibe and structure of your assistant’s replies—clear and friendly? Short and snappy? Detailed and formal?",
|
||||||
|
"BREADCRUMB": {
|
||||||
|
"TITLE": "Response Guidelines"
|
||||||
|
},
|
||||||
|
"BULK_ACTION": {
|
||||||
|
"SELECTED": "{count} item selected | {count} items selected",
|
||||||
|
"SELECT_ALL": "Select all ({count})",
|
||||||
|
"UNSELECT_ALL": "Unselect all ({count})",
|
||||||
|
"BULK_DELETE_BUTTON": "Delete"
|
||||||
|
},
|
||||||
|
"ADD": {
|
||||||
|
"SUGGESTED": {
|
||||||
|
"TITLE": "Example response guidelines",
|
||||||
|
"ADD": "Add all",
|
||||||
|
"ADD_SINGLE": "Add this",
|
||||||
|
"SAVE": "Add and save (↵)",
|
||||||
|
"PLACEHOLDER": "Type in another response guideline..."
|
||||||
|
},
|
||||||
|
"NEW": {
|
||||||
|
"TITLE": "Add a response guideline",
|
||||||
|
"CREATE": "Create",
|
||||||
|
"CANCEL": "Cancel",
|
||||||
|
"PLACEHOLDER": "Type in another response guideline...",
|
||||||
|
"TEST_ALL": "Test all"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LIST": {
|
||||||
|
"SEARCH_PLACEHOLDER": "Search..."
|
||||||
|
},
|
||||||
|
"EMPTY_MESSAGE": "No response guidelines found. Create or add examples to begin.",
|
||||||
|
"API": {
|
||||||
|
"ADD": {
|
||||||
|
"SUCCESS": "Response Guidelines added successfully",
|
||||||
|
"ERROR": "There was an error adding response guidelines, please try again."
|
||||||
|
},
|
||||||
|
"UPDATE": {
|
||||||
|
"SUCCESS": "Response Guidelines updated successfully",
|
||||||
|
"ERROR": "There was an error updating response guidelines, please try again."
|
||||||
|
},
|
||||||
|
"DELETE": {
|
||||||
|
"SUCCESS": "Response Guidelines deleted successfully",
|
||||||
|
"ERROR": "There was an error deleting response guidelines, please try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"DOCUMENTS": {
|
"DOCUMENTS": {
|
||||||
|
|||||||
@@ -319,6 +319,7 @@
|
|||||||
"CSAT": "CSAT",
|
"CSAT": "CSAT",
|
||||||
"LIVE_CHAT": "Live Chat",
|
"LIVE_CHAT": "Live Chat",
|
||||||
"SMS": "SMS",
|
"SMS": "SMS",
|
||||||
|
"WHATSAPP": "WhatsApp",
|
||||||
"CAMPAIGNS": "Campaigns",
|
"CAMPAIGNS": "Campaigns",
|
||||||
"ONGOING": "Ongoing",
|
"ONGOING": "Ongoing",
|
||||||
"ONE_OFF": "One off",
|
"ONE_OFF": "One off",
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
"PICKER": {
|
"PICKER": {
|
||||||
"SEARCH_PLACEHOLDER": "Search Templates",
|
"SEARCH_PLACEHOLDER": "Search Templates",
|
||||||
"NO_TEMPLATES_FOUND": "No templates found for",
|
"NO_TEMPLATES_FOUND": "No templates found for",
|
||||||
|
"NO_TEMPLATES_AVAILABLE": "No WhatsApp templates available. Click refresh to sync templates from WhatsApp.",
|
||||||
|
"REFRESH_BUTTON": "Refresh templates",
|
||||||
|
"REFRESH_SUCCESS": "Templates refresh initiated. It may take a couple of minutes to update.",
|
||||||
|
"REFRESH_ERROR": "Failed to refresh templates. Please try again.",
|
||||||
"LABELS": {
|
"LABELS": {
|
||||||
"LANGUAGE": "Language",
|
"LANGUAGE": "Language",
|
||||||
"TEMPLATE_BODY": "Template Body",
|
"TEMPLATE_BODY": "Template Body",
|
||||||
|
|||||||
@@ -146,6 +146,7 @@
|
|||||||
"SEND_WEBHOOK_EVENT": "Send Webhook Event",
|
"SEND_WEBHOOK_EVENT": "Send Webhook Event",
|
||||||
"SEND_ATTACHMENT": "Send Attachment",
|
"SEND_ATTACHMENT": "Send Attachment",
|
||||||
"SEND_MESSAGE": "Send a Message",
|
"SEND_MESSAGE": "Send a Message",
|
||||||
|
"ADD_PRIVATE_NOTE": "Add a Private Note",
|
||||||
"CHANGE_PRIORITY": "Change Priority",
|
"CHANGE_PRIORITY": "Change Priority",
|
||||||
"ADD_SLA": "Add SLA",
|
"ADD_SLA": "Add SLA",
|
||||||
"OPEN_CONVERSATION": "Open conversation"
|
"OPEN_CONVERSATION": "Open conversation"
|
||||||
|
|||||||
@@ -137,6 +137,70 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"WHATSAPP": {
|
||||||
|
"HEADER_TITLE": "WhatsApp campaigns",
|
||||||
|
"NEW_CAMPAIGN": "Create campaign",
|
||||||
|
"EMPTY_STATE": {
|
||||||
|
"TITLE": "No WhatsApp campaigns are available",
|
||||||
|
"SUBTITLE": "Launch a WhatsApp campaign to reach your customers directly. Send offers or make announcements with ease. Click 'Create campaign' to get started."
|
||||||
|
},
|
||||||
|
"CARD": {
|
||||||
|
"STATUS": {
|
||||||
|
"COMPLETED": "Завършено",
|
||||||
|
"SCHEDULED": "Scheduled"
|
||||||
|
},
|
||||||
|
"CAMPAIGN_DETAILS": {
|
||||||
|
"SENT_FROM": "Sent from",
|
||||||
|
"ON": "on"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CREATE": {
|
||||||
|
"TITLE": "Create WhatsApp campaign",
|
||||||
|
"CANCEL_BUTTON_TEXT": "Отмени",
|
||||||
|
"CREATE_BUTTON_TEXT": "Create",
|
||||||
|
"FORM": {
|
||||||
|
"TITLE": {
|
||||||
|
"LABEL": "Title",
|
||||||
|
"PLACEHOLDER": "Please enter the title of campaign",
|
||||||
|
"ERROR": "Title is required"
|
||||||
|
},
|
||||||
|
"INBOX": {
|
||||||
|
"LABEL": "Select Inbox",
|
||||||
|
"PLACEHOLDER": "Select Inbox",
|
||||||
|
"ERROR": "Inbox is required"
|
||||||
|
},
|
||||||
|
"TEMPLATE": {
|
||||||
|
"LABEL": "WhatsApp Template",
|
||||||
|
"PLACEHOLDER": "Select a template",
|
||||||
|
"INFO": "Select a template to use for this campaign.",
|
||||||
|
"ERROR": "Template is required",
|
||||||
|
"PREVIEW_TITLE": "Process {templateName}",
|
||||||
|
"LANGUAGE": "Language",
|
||||||
|
"CATEGORY": "Category",
|
||||||
|
"VARIABLES_LABEL": "Variables",
|
||||||
|
"VARIABLE_PLACEHOLDER": "Enter value for {variable}"
|
||||||
|
},
|
||||||
|
"AUDIENCE": {
|
||||||
|
"LABEL": "Audience",
|
||||||
|
"PLACEHOLDER": "Select the customer labels",
|
||||||
|
"ERROR": "Audience is required"
|
||||||
|
},
|
||||||
|
"SCHEDULED_AT": {
|
||||||
|
"LABEL": "Scheduled time",
|
||||||
|
"PLACEHOLDER": "Please select the time",
|
||||||
|
"ERROR": "Scheduled time is required"
|
||||||
|
},
|
||||||
|
"BUTTONS": {
|
||||||
|
"CREATE": "Create",
|
||||||
|
"CANCEL": "Отмени"
|
||||||
|
},
|
||||||
|
"API": {
|
||||||
|
"SUCCESS_MESSAGE": "WhatsApp campaign created successfully",
|
||||||
|
"ERROR_MESSAGE": "There was an error. Please try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"CONFIRM_DELETE": {
|
"CONFIRM_DELETE": {
|
||||||
"TITLE": "Сигурни ли сте за изтриването?",
|
"TITLE": "Сигурни ли сте за изтриването?",
|
||||||
"DESCRIPTION": "The delete action is permanent and cannot be reversed.",
|
"DESCRIPTION": "The delete action is permanent and cannot be reversed.",
|
||||||
|
|||||||
@@ -222,10 +222,17 @@
|
|||||||
"DESC": "Start supporting your customers via WhatsApp.",
|
"DESC": "Start supporting your customers via WhatsApp.",
|
||||||
"PROVIDERS": {
|
"PROVIDERS": {
|
||||||
"LABEL": "API Provider",
|
"LABEL": "API Provider",
|
||||||
|
"WHATSAPP_EMBEDDED": "WhatsApp Business",
|
||||||
"TWILIO": "Twilio",
|
"TWILIO": "Twilio",
|
||||||
"WHATSAPP_CLOUD": "WhatsApp Cloud",
|
"WHATSAPP_CLOUD": "WhatsApp Cloud",
|
||||||
|
"WHATSAPP_CLOUD_DESC": "Quick setup through Meta",
|
||||||
|
"TWILIO_DESC": "Connect via Twilio credentials",
|
||||||
"360_DIALOG": "360Dialog"
|
"360_DIALOG": "360Dialog"
|
||||||
},
|
},
|
||||||
|
"SELECT_PROVIDER": {
|
||||||
|
"TITLE": "Select your API provider",
|
||||||
|
"DESCRIPTION": "Choose your WhatsApp provider. You can connect directly through Meta which requires no setup, or connect through Twilio using your account credentials."
|
||||||
|
},
|
||||||
"INBOX_NAME": {
|
"INBOX_NAME": {
|
||||||
"LABEL": "Inbox Name",
|
"LABEL": "Inbox Name",
|
||||||
"PLACEHOLDER": "Please enter an inbox name",
|
"PLACEHOLDER": "Please enter an inbox name",
|
||||||
@@ -264,6 +271,28 @@
|
|||||||
"WEBHOOK_VERIFICATION_TOKEN": "Webhook Verification Token"
|
"WEBHOOK_VERIFICATION_TOKEN": "Webhook Verification Token"
|
||||||
},
|
},
|
||||||
"SUBMIT_BUTTON": "Create WhatsApp Channel",
|
"SUBMIT_BUTTON": "Create WhatsApp Channel",
|
||||||
|
"EMBEDDED_SIGNUP": {
|
||||||
|
"TITLE": "Quick Setup with Meta",
|
||||||
|
"DESC": "You will be redirected to Meta to log into your WhatsApp Business account. Having admin access will help make the setup smooth and easy.",
|
||||||
|
"BENEFITS": {
|
||||||
|
"TITLE": "Benefits of Embedded Signup:",
|
||||||
|
"EASY_SETUP": "No manual configuration required",
|
||||||
|
"SECURE_AUTH": "Secure OAuth based authentication",
|
||||||
|
"AUTO_CONFIG": "Automatic webhook and phone number configuration"
|
||||||
|
},
|
||||||
|
"SUBMIT_BUTTON": "Connect with WhatsApp Business",
|
||||||
|
"AUTH_PROCESSING": "Authenticating with Meta",
|
||||||
|
"WAITING_FOR_BUSINESS_INFO": "Please complete business setup in the Meta window...",
|
||||||
|
"PROCESSING": "Setting up your WhatsApp Business Account",
|
||||||
|
"LOADING_SDK": "Loading Facebook SDK...",
|
||||||
|
"CANCELLED": "WhatsApp Signup was cancelled",
|
||||||
|
"SUCCESS_TITLE": "WhatsApp Business Account Connected!",
|
||||||
|
"WAITING_FOR_AUTH": "Waiting for authentication...",
|
||||||
|
"INVALID_BUSINESS_DATA": "Invalid business data received from Facebook. Please try again.",
|
||||||
|
"SIGNUP_ERROR": "Signup error occurred",
|
||||||
|
"AUTH_NOT_COMPLETED": "Authentication not completed. Please restart the process.",
|
||||||
|
"SUCCESS_FALLBACK": "WhatsApp Business Account has been successfully configured"
|
||||||
|
},
|
||||||
"API": {
|
"API": {
|
||||||
"ERROR_MESSAGE": "We were not able to save the WhatsApp channel"
|
"ERROR_MESSAGE": "We were not able to save the WhatsApp channel"
|
||||||
}
|
}
|
||||||
@@ -571,6 +600,10 @@
|
|||||||
"WHATSAPP_SECTION_UPDATE_BUTTON": "Обновяване",
|
"WHATSAPP_SECTION_UPDATE_BUTTON": "Обновяване",
|
||||||
"WHATSAPP_WEBHOOK_TITLE": "Webhook Verification Token",
|
"WHATSAPP_WEBHOOK_TITLE": "Webhook Verification Token",
|
||||||
"WHATSAPP_WEBHOOK_SUBHEADER": "This token is used to verify the authenticity of the webhook endpoint.",
|
"WHATSAPP_WEBHOOK_SUBHEADER": "This token is used to verify the authenticity of the webhook endpoint.",
|
||||||
|
"WHATSAPP_TEMPLATES_SYNC_TITLE": "Sync Templates",
|
||||||
|
"WHATSAPP_TEMPLATES_SYNC_SUBHEADER": "Manually sync message templates from WhatsApp to update your available templates.",
|
||||||
|
"WHATSAPP_TEMPLATES_SYNC_BUTTON": "Sync Templates",
|
||||||
|
"WHATSAPP_TEMPLATES_SYNC_SUCCESS": "Templates sync initiated successfully. It may take a couple of minutes to update.",
|
||||||
"UPDATE_PRE_CHAT_FORM_SETTINGS": "Update Pre Chat Form Settings"
|
"UPDATE_PRE_CHAT_FORM_SETTINGS": "Update Pre Chat Form Settings"
|
||||||
},
|
},
|
||||||
"HELP_CENTER": {
|
"HELP_CENTER": {
|
||||||
|
|||||||
@@ -524,6 +524,100 @@
|
|||||||
"TITLE": "Captain Assistant",
|
"TITLE": "Captain Assistant",
|
||||||
"NOTE": "Captain Assistant engages directly with customers, learns from your help docs and past conversations, and delivers instant, accurate responses. It handles the initial queries, providing quick resolutions before transferring to an agent when needed."
|
"NOTE": "Captain Assistant engages directly with customers, learns from your help docs and past conversations, and delivers instant, accurate responses. It handles the initial queries, providing quick resolutions before transferring to an agent when needed."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"GUARDRAILS": {
|
||||||
|
"TITLE": "Guardrails",
|
||||||
|
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic.",
|
||||||
|
"BREADCRUMB": {
|
||||||
|
"TITLE": "Guardrails"
|
||||||
|
},
|
||||||
|
"BULK_ACTION": {
|
||||||
|
"SELECTED": "{count} item selected | {count} items selected",
|
||||||
|
"SELECT_ALL": "Select all ({count})",
|
||||||
|
"UNSELECT_ALL": "Unselect all ({count})",
|
||||||
|
"BULK_DELETE_BUTTON": "Изтрий"
|
||||||
|
},
|
||||||
|
"ADD": {
|
||||||
|
"SUGGESTED": {
|
||||||
|
"TITLE": "Example guardrails",
|
||||||
|
"ADD": "Add all",
|
||||||
|
"ADD_SINGLE": "Add this",
|
||||||
|
"SAVE": "Add and save (↵)",
|
||||||
|
"PLACEHOLDER": "Type in another guardrail..."
|
||||||
|
},
|
||||||
|
"NEW": {
|
||||||
|
"TITLE": "Add a guardrail",
|
||||||
|
"CREATE": "Create",
|
||||||
|
"CANCEL": "Отмени",
|
||||||
|
"PLACEHOLDER": "Type in another guardrail...",
|
||||||
|
"TEST_ALL": "Test all"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LIST": {
|
||||||
|
"SEARCH_PLACEHOLDER": "Search..."
|
||||||
|
},
|
||||||
|
"EMPTY_MESSAGE": "No guardrails found. Create or add examples to begin.",
|
||||||
|
"API": {
|
||||||
|
"ADD": {
|
||||||
|
"SUCCESS": "Guardrails added successfully",
|
||||||
|
"ERROR": "There was an error adding guardrails, please try again."
|
||||||
|
},
|
||||||
|
"UPDATE": {
|
||||||
|
"SUCCESS": "Guardrails updated successfully",
|
||||||
|
"ERROR": "There was an error updating guardrails, please try again."
|
||||||
|
},
|
||||||
|
"DELETE": {
|
||||||
|
"SUCCESS": "Guardrails deleted successfully",
|
||||||
|
"ERROR": "There was an error deleting guardrails, please try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RESPONSE_GUIDELINES": {
|
||||||
|
"TITLE": "Response Guidelines",
|
||||||
|
"DESCRIPTION": "The vibe and structure of your assistant’s replies—clear and friendly? Short and snappy? Detailed and formal?",
|
||||||
|
"BREADCRUMB": {
|
||||||
|
"TITLE": "Response Guidelines"
|
||||||
|
},
|
||||||
|
"BULK_ACTION": {
|
||||||
|
"SELECTED": "{count} item selected | {count} items selected",
|
||||||
|
"SELECT_ALL": "Select all ({count})",
|
||||||
|
"UNSELECT_ALL": "Unselect all ({count})",
|
||||||
|
"BULK_DELETE_BUTTON": "Изтрий"
|
||||||
|
},
|
||||||
|
"ADD": {
|
||||||
|
"SUGGESTED": {
|
||||||
|
"TITLE": "Example response guidelines",
|
||||||
|
"ADD": "Add all",
|
||||||
|
"ADD_SINGLE": "Add this",
|
||||||
|
"SAVE": "Add and save (↵)",
|
||||||
|
"PLACEHOLDER": "Type in another response guideline..."
|
||||||
|
},
|
||||||
|
"NEW": {
|
||||||
|
"TITLE": "Add a response guideline",
|
||||||
|
"CREATE": "Create",
|
||||||
|
"CANCEL": "Отмени",
|
||||||
|
"PLACEHOLDER": "Type in another response guideline...",
|
||||||
|
"TEST_ALL": "Test all"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LIST": {
|
||||||
|
"SEARCH_PLACEHOLDER": "Search..."
|
||||||
|
},
|
||||||
|
"EMPTY_MESSAGE": "No response guidelines found. Create or add examples to begin.",
|
||||||
|
"API": {
|
||||||
|
"ADD": {
|
||||||
|
"SUCCESS": "Response Guidelines added successfully",
|
||||||
|
"ERROR": "There was an error adding response guidelines, please try again."
|
||||||
|
},
|
||||||
|
"UPDATE": {
|
||||||
|
"SUCCESS": "Response Guidelines updated successfully",
|
||||||
|
"ERROR": "There was an error updating response guidelines, please try again."
|
||||||
|
},
|
||||||
|
"DELETE": {
|
||||||
|
"SUCCESS": "Response Guidelines deleted successfully",
|
||||||
|
"ERROR": "There was an error deleting response guidelines, please try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"DOCUMENTS": {
|
"DOCUMENTS": {
|
||||||
|
|||||||
@@ -319,6 +319,7 @@
|
|||||||
"CSAT": "CSAT",
|
"CSAT": "CSAT",
|
||||||
"LIVE_CHAT": "Live Chat",
|
"LIVE_CHAT": "Live Chat",
|
||||||
"SMS": "SMS",
|
"SMS": "SMS",
|
||||||
|
"WHATSAPP": "WhatsApp",
|
||||||
"CAMPAIGNS": "Campaigns",
|
"CAMPAIGNS": "Campaigns",
|
||||||
"ONGOING": "Ongoing",
|
"ONGOING": "Ongoing",
|
||||||
"ONE_OFF": "One off",
|
"ONE_OFF": "One off",
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
"PICKER": {
|
"PICKER": {
|
||||||
"SEARCH_PLACEHOLDER": "Search Templates",
|
"SEARCH_PLACEHOLDER": "Search Templates",
|
||||||
"NO_TEMPLATES_FOUND": "No templates found for",
|
"NO_TEMPLATES_FOUND": "No templates found for",
|
||||||
|
"NO_TEMPLATES_AVAILABLE": "No WhatsApp templates available. Click refresh to sync templates from WhatsApp.",
|
||||||
|
"REFRESH_BUTTON": "Refresh templates",
|
||||||
|
"REFRESH_SUCCESS": "Templates refresh initiated. It may take a couple of minutes to update.",
|
||||||
|
"REFRESH_ERROR": "Failed to refresh templates. Please try again.",
|
||||||
"LABELS": {
|
"LABELS": {
|
||||||
"LANGUAGE": "Language",
|
"LANGUAGE": "Language",
|
||||||
"TEMPLATE_BODY": "Template Body",
|
"TEMPLATE_BODY": "Template Body",
|
||||||
|
|||||||
@@ -146,6 +146,7 @@
|
|||||||
"SEND_WEBHOOK_EVENT": "Send Webhook Event",
|
"SEND_WEBHOOK_EVENT": "Send Webhook Event",
|
||||||
"SEND_ATTACHMENT": "Send Attachment",
|
"SEND_ATTACHMENT": "Send Attachment",
|
||||||
"SEND_MESSAGE": "Send a Message",
|
"SEND_MESSAGE": "Send a Message",
|
||||||
|
"ADD_PRIVATE_NOTE": "Add a Private Note",
|
||||||
"CHANGE_PRIORITY": "Canvia la prioritat",
|
"CHANGE_PRIORITY": "Canvia la prioritat",
|
||||||
"ADD_SLA": "Afegeix SLA",
|
"ADD_SLA": "Afegeix SLA",
|
||||||
"OPEN_CONVERSATION": "Obrir conversa"
|
"OPEN_CONVERSATION": "Obrir conversa"
|
||||||
|
|||||||
@@ -137,6 +137,70 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"WHATSAPP": {
|
||||||
|
"HEADER_TITLE": "WhatsApp campaigns",
|
||||||
|
"NEW_CAMPAIGN": "Create campaign",
|
||||||
|
"EMPTY_STATE": {
|
||||||
|
"TITLE": "No WhatsApp campaigns are available",
|
||||||
|
"SUBTITLE": "Launch a WhatsApp campaign to reach your customers directly. Send offers or make announcements with ease. Click 'Create campaign' to get started."
|
||||||
|
},
|
||||||
|
"CARD": {
|
||||||
|
"STATUS": {
|
||||||
|
"COMPLETED": "Completat",
|
||||||
|
"SCHEDULED": "Scheduled"
|
||||||
|
},
|
||||||
|
"CAMPAIGN_DETAILS": {
|
||||||
|
"SENT_FROM": "Sent from",
|
||||||
|
"ON": "on"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CREATE": {
|
||||||
|
"TITLE": "Create WhatsApp campaign",
|
||||||
|
"CANCEL_BUTTON_TEXT": "Cancel·la",
|
||||||
|
"CREATE_BUTTON_TEXT": "Crear",
|
||||||
|
"FORM": {
|
||||||
|
"TITLE": {
|
||||||
|
"LABEL": "Títol",
|
||||||
|
"PLACEHOLDER": "Introduïu el títol de la campanya",
|
||||||
|
"ERROR": "El títol és necessari"
|
||||||
|
},
|
||||||
|
"INBOX": {
|
||||||
|
"LABEL": "Selecciona Safata d'entrada",
|
||||||
|
"PLACEHOLDER": "Selecciona Safata d'entrada",
|
||||||
|
"ERROR": "Safata d'entrada és necessària"
|
||||||
|
},
|
||||||
|
"TEMPLATE": {
|
||||||
|
"LABEL": "WhatsApp Template",
|
||||||
|
"PLACEHOLDER": "Select a template",
|
||||||
|
"INFO": "Select a template to use for this campaign.",
|
||||||
|
"ERROR": "Template is required",
|
||||||
|
"PREVIEW_TITLE": "Procés {templateName}",
|
||||||
|
"LANGUAGE": "Idioma",
|
||||||
|
"CATEGORY": "Categoria",
|
||||||
|
"VARIABLES_LABEL": "Variables",
|
||||||
|
"VARIABLE_PLACEHOLDER": "Enter value for {variable}"
|
||||||
|
},
|
||||||
|
"AUDIENCE": {
|
||||||
|
"LABEL": "Públic",
|
||||||
|
"PLACEHOLDER": "Selecciona les etiquetes dels clients",
|
||||||
|
"ERROR": "L'audiència és obligatòria"
|
||||||
|
},
|
||||||
|
"SCHEDULED_AT": {
|
||||||
|
"LABEL": "Hora programada",
|
||||||
|
"PLACEHOLDER": "Selecciona l'hora",
|
||||||
|
"ERROR": "Es requereix hora programada"
|
||||||
|
},
|
||||||
|
"BUTTONS": {
|
||||||
|
"CREATE": "Crear",
|
||||||
|
"CANCEL": "Cancel·la"
|
||||||
|
},
|
||||||
|
"API": {
|
||||||
|
"SUCCESS_MESSAGE": "WhatsApp campaign created successfully",
|
||||||
|
"ERROR_MESSAGE": "S'ha produït un error. Torna-ho a provar."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"CONFIRM_DELETE": {
|
"CONFIRM_DELETE": {
|
||||||
"TITLE": "N'estàs segur?",
|
"TITLE": "N'estàs segur?",
|
||||||
"DESCRIPTION": "The delete action is permanent and cannot be reversed.",
|
"DESCRIPTION": "The delete action is permanent and cannot be reversed.",
|
||||||
|
|||||||
@@ -222,10 +222,17 @@
|
|||||||
"DESC": "Comença a donar suport als teus clients mitjançant WhatsApp.",
|
"DESC": "Comença a donar suport als teus clients mitjançant WhatsApp.",
|
||||||
"PROVIDERS": {
|
"PROVIDERS": {
|
||||||
"LABEL": "Proveïdor d'API",
|
"LABEL": "Proveïdor d'API",
|
||||||
|
"WHATSAPP_EMBEDDED": "WhatsApp Business",
|
||||||
"TWILIO": "Twilio",
|
"TWILIO": "Twilio",
|
||||||
"WHATSAPP_CLOUD": "WhatsApp Cloud",
|
"WHATSAPP_CLOUD": "WhatsApp Cloud",
|
||||||
|
"WHATSAPP_CLOUD_DESC": "Quick setup through Meta",
|
||||||
|
"TWILIO_DESC": "Connect via Twilio credentials",
|
||||||
"360_DIALOG": "360dialog"
|
"360_DIALOG": "360dialog"
|
||||||
},
|
},
|
||||||
|
"SELECT_PROVIDER": {
|
||||||
|
"TITLE": "Select your API provider",
|
||||||
|
"DESCRIPTION": "Choose your WhatsApp provider. You can connect directly through Meta which requires no setup, or connect through Twilio using your account credentials."
|
||||||
|
},
|
||||||
"INBOX_NAME": {
|
"INBOX_NAME": {
|
||||||
"LABEL": "Nom de la safata d'entrada",
|
"LABEL": "Nom de la safata d'entrada",
|
||||||
"PLACEHOLDER": "Introdueix un nom de safata d'entrada",
|
"PLACEHOLDER": "Introdueix un nom de safata d'entrada",
|
||||||
@@ -264,6 +271,28 @@
|
|||||||
"WEBHOOK_VERIFICATION_TOKEN": "Token de verificació del webhook"
|
"WEBHOOK_VERIFICATION_TOKEN": "Token de verificació del webhook"
|
||||||
},
|
},
|
||||||
"SUBMIT_BUTTON": "Crea un canal de WhatsApp",
|
"SUBMIT_BUTTON": "Crea un canal de WhatsApp",
|
||||||
|
"EMBEDDED_SIGNUP": {
|
||||||
|
"TITLE": "Quick Setup with Meta",
|
||||||
|
"DESC": "You will be redirected to Meta to log into your WhatsApp Business account. Having admin access will help make the setup smooth and easy.",
|
||||||
|
"BENEFITS": {
|
||||||
|
"TITLE": "Benefits of Embedded Signup:",
|
||||||
|
"EASY_SETUP": "No manual configuration required",
|
||||||
|
"SECURE_AUTH": "Secure OAuth based authentication",
|
||||||
|
"AUTO_CONFIG": "Automatic webhook and phone number configuration"
|
||||||
|
},
|
||||||
|
"SUBMIT_BUTTON": "Connect with WhatsApp Business",
|
||||||
|
"AUTH_PROCESSING": "Authenticating with Meta",
|
||||||
|
"WAITING_FOR_BUSINESS_INFO": "Please complete business setup in the Meta window...",
|
||||||
|
"PROCESSING": "Setting up your WhatsApp Business Account",
|
||||||
|
"LOADING_SDK": "Loading Facebook SDK...",
|
||||||
|
"CANCELLED": "WhatsApp Signup was cancelled",
|
||||||
|
"SUCCESS_TITLE": "WhatsApp Business Account Connected!",
|
||||||
|
"WAITING_FOR_AUTH": "Waiting for authentication...",
|
||||||
|
"INVALID_BUSINESS_DATA": "Invalid business data received from Facebook. Please try again.",
|
||||||
|
"SIGNUP_ERROR": "Signup error occurred",
|
||||||
|
"AUTH_NOT_COMPLETED": "Authentication not completed. Please restart the process.",
|
||||||
|
"SUCCESS_FALLBACK": "WhatsApp Business Account has been successfully configured"
|
||||||
|
},
|
||||||
"API": {
|
"API": {
|
||||||
"ERROR_MESSAGE": "No hem pogut desar el canal WhatsApp"
|
"ERROR_MESSAGE": "No hem pogut desar el canal WhatsApp"
|
||||||
}
|
}
|
||||||
@@ -571,6 +600,10 @@
|
|||||||
"WHATSAPP_SECTION_UPDATE_BUTTON": "Actualitza",
|
"WHATSAPP_SECTION_UPDATE_BUTTON": "Actualitza",
|
||||||
"WHATSAPP_WEBHOOK_TITLE": "Token de verificació del webhook",
|
"WHATSAPP_WEBHOOK_TITLE": "Token de verificació del webhook",
|
||||||
"WHATSAPP_WEBHOOK_SUBHEADER": "Aquest testimoni s'utilitza per verificar l'autenticitat del punt final del webhook.",
|
"WHATSAPP_WEBHOOK_SUBHEADER": "Aquest testimoni s'utilitza per verificar l'autenticitat del punt final del webhook.",
|
||||||
|
"WHATSAPP_TEMPLATES_SYNC_TITLE": "Sync Templates",
|
||||||
|
"WHATSAPP_TEMPLATES_SYNC_SUBHEADER": "Manually sync message templates from WhatsApp to update your available templates.",
|
||||||
|
"WHATSAPP_TEMPLATES_SYNC_BUTTON": "Sync Templates",
|
||||||
|
"WHATSAPP_TEMPLATES_SYNC_SUCCESS": "Templates sync initiated successfully. It may take a couple of minutes to update.",
|
||||||
"UPDATE_PRE_CHAT_FORM_SETTINGS": "Actualitza la configuració del formulari de xat prèvia"
|
"UPDATE_PRE_CHAT_FORM_SETTINGS": "Actualitza la configuració del formulari de xat prèvia"
|
||||||
},
|
},
|
||||||
"HELP_CENTER": {
|
"HELP_CENTER": {
|
||||||
|
|||||||
@@ -524,6 +524,100 @@
|
|||||||
"TITLE": "Captain Assistant",
|
"TITLE": "Captain Assistant",
|
||||||
"NOTE": "Captain Assistant engages directly with customers, learns from your help docs and past conversations, and delivers instant, accurate responses. It handles the initial queries, providing quick resolutions before transferring to an agent when needed."
|
"NOTE": "Captain Assistant engages directly with customers, learns from your help docs and past conversations, and delivers instant, accurate responses. It handles the initial queries, providing quick resolutions before transferring to an agent when needed."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"GUARDRAILS": {
|
||||||
|
"TITLE": "Guardrails",
|
||||||
|
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic.",
|
||||||
|
"BREADCRUMB": {
|
||||||
|
"TITLE": "Guardrails"
|
||||||
|
},
|
||||||
|
"BULK_ACTION": {
|
||||||
|
"SELECTED": "{count} item selected | {count} items selected",
|
||||||
|
"SELECT_ALL": "Select all ({count})",
|
||||||
|
"UNSELECT_ALL": "Unselect all ({count})",
|
||||||
|
"BULK_DELETE_BUTTON": "Esborrar"
|
||||||
|
},
|
||||||
|
"ADD": {
|
||||||
|
"SUGGESTED": {
|
||||||
|
"TITLE": "Example guardrails",
|
||||||
|
"ADD": "Add all",
|
||||||
|
"ADD_SINGLE": "Add this",
|
||||||
|
"SAVE": "Add and save (↵)",
|
||||||
|
"PLACEHOLDER": "Type in another guardrail..."
|
||||||
|
},
|
||||||
|
"NEW": {
|
||||||
|
"TITLE": "Add a guardrail",
|
||||||
|
"CREATE": "Crear",
|
||||||
|
"CANCEL": "Cancel·la",
|
||||||
|
"PLACEHOLDER": "Type in another guardrail...",
|
||||||
|
"TEST_ALL": "Test all"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LIST": {
|
||||||
|
"SEARCH_PLACEHOLDER": "Search..."
|
||||||
|
},
|
||||||
|
"EMPTY_MESSAGE": "No guardrails found. Create or add examples to begin.",
|
||||||
|
"API": {
|
||||||
|
"ADD": {
|
||||||
|
"SUCCESS": "Guardrails added successfully",
|
||||||
|
"ERROR": "There was an error adding guardrails, please try again."
|
||||||
|
},
|
||||||
|
"UPDATE": {
|
||||||
|
"SUCCESS": "Guardrails updated successfully",
|
||||||
|
"ERROR": "There was an error updating guardrails, please try again."
|
||||||
|
},
|
||||||
|
"DELETE": {
|
||||||
|
"SUCCESS": "Guardrails deleted successfully",
|
||||||
|
"ERROR": "There was an error deleting guardrails, please try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RESPONSE_GUIDELINES": {
|
||||||
|
"TITLE": "Response Guidelines",
|
||||||
|
"DESCRIPTION": "The vibe and structure of your assistant’s replies—clear and friendly? Short and snappy? Detailed and formal?",
|
||||||
|
"BREADCRUMB": {
|
||||||
|
"TITLE": "Response Guidelines"
|
||||||
|
},
|
||||||
|
"BULK_ACTION": {
|
||||||
|
"SELECTED": "{count} item selected | {count} items selected",
|
||||||
|
"SELECT_ALL": "Select all ({count})",
|
||||||
|
"UNSELECT_ALL": "Unselect all ({count})",
|
||||||
|
"BULK_DELETE_BUTTON": "Esborrar"
|
||||||
|
},
|
||||||
|
"ADD": {
|
||||||
|
"SUGGESTED": {
|
||||||
|
"TITLE": "Example response guidelines",
|
||||||
|
"ADD": "Add all",
|
||||||
|
"ADD_SINGLE": "Add this",
|
||||||
|
"SAVE": "Add and save (↵)",
|
||||||
|
"PLACEHOLDER": "Type in another response guideline..."
|
||||||
|
},
|
||||||
|
"NEW": {
|
||||||
|
"TITLE": "Add a response guideline",
|
||||||
|
"CREATE": "Crear",
|
||||||
|
"CANCEL": "Cancel·la",
|
||||||
|
"PLACEHOLDER": "Type in another response guideline...",
|
||||||
|
"TEST_ALL": "Test all"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LIST": {
|
||||||
|
"SEARCH_PLACEHOLDER": "Search..."
|
||||||
|
},
|
||||||
|
"EMPTY_MESSAGE": "No response guidelines found. Create or add examples to begin.",
|
||||||
|
"API": {
|
||||||
|
"ADD": {
|
||||||
|
"SUCCESS": "Response Guidelines added successfully",
|
||||||
|
"ERROR": "There was an error adding response guidelines, please try again."
|
||||||
|
},
|
||||||
|
"UPDATE": {
|
||||||
|
"SUCCESS": "Response Guidelines updated successfully",
|
||||||
|
"ERROR": "There was an error updating response guidelines, please try again."
|
||||||
|
},
|
||||||
|
"DELETE": {
|
||||||
|
"SUCCESS": "Response Guidelines deleted successfully",
|
||||||
|
"ERROR": "There was an error deleting response guidelines, please try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"DOCUMENTS": {
|
"DOCUMENTS": {
|
||||||
|
|||||||
@@ -319,6 +319,7 @@
|
|||||||
"CSAT": "CSAT",
|
"CSAT": "CSAT",
|
||||||
"LIVE_CHAT": "Live Chat",
|
"LIVE_CHAT": "Live Chat",
|
||||||
"SMS": "SMS",
|
"SMS": "SMS",
|
||||||
|
"WHATSAPP": "WhatsApp",
|
||||||
"CAMPAIGNS": "Campanyes",
|
"CAMPAIGNS": "Campanyes",
|
||||||
"ONGOING": "En marxa",
|
"ONGOING": "En marxa",
|
||||||
"ONE_OFF": "Un fora",
|
"ONE_OFF": "Un fora",
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
"PICKER": {
|
"PICKER": {
|
||||||
"SEARCH_PLACEHOLDER": "Cerca plantilles",
|
"SEARCH_PLACEHOLDER": "Cerca plantilles",
|
||||||
"NO_TEMPLATES_FOUND": "No s'han trobat plantilles per a",
|
"NO_TEMPLATES_FOUND": "No s'han trobat plantilles per a",
|
||||||
|
"NO_TEMPLATES_AVAILABLE": "No WhatsApp templates available. Click refresh to sync templates from WhatsApp.",
|
||||||
|
"REFRESH_BUTTON": "Refresh templates",
|
||||||
|
"REFRESH_SUCCESS": "Templates refresh initiated. It may take a couple of minutes to update.",
|
||||||
|
"REFRESH_ERROR": "Failed to refresh templates. Please try again.",
|
||||||
"LABELS": {
|
"LABELS": {
|
||||||
"LANGUAGE": "Idioma",
|
"LANGUAGE": "Idioma",
|
||||||
"TEMPLATE_BODY": "Cos de la plantilla",
|
"TEMPLATE_BODY": "Cos de la plantilla",
|
||||||
|
|||||||
@@ -146,6 +146,7 @@
|
|||||||
"SEND_WEBHOOK_EVENT": "Send Webhook Event",
|
"SEND_WEBHOOK_EVENT": "Send Webhook Event",
|
||||||
"SEND_ATTACHMENT": "Odeslat přílohu",
|
"SEND_ATTACHMENT": "Odeslat přílohu",
|
||||||
"SEND_MESSAGE": "Odeslat zprávu",
|
"SEND_MESSAGE": "Odeslat zprávu",
|
||||||
|
"ADD_PRIVATE_NOTE": "Přidat soukromou poznámku",
|
||||||
"CHANGE_PRIORITY": "Change Priority",
|
"CHANGE_PRIORITY": "Change Priority",
|
||||||
"ADD_SLA": "Add SLA",
|
"ADD_SLA": "Add SLA",
|
||||||
"OPEN_CONVERSATION": "Open conversation"
|
"OPEN_CONVERSATION": "Open conversation"
|
||||||
|
|||||||
@@ -137,6 +137,70 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"WHATSAPP": {
|
||||||
|
"HEADER_TITLE": "WhatsApp campaigns",
|
||||||
|
"NEW_CAMPAIGN": "Create campaign",
|
||||||
|
"EMPTY_STATE": {
|
||||||
|
"TITLE": "No WhatsApp campaigns are available",
|
||||||
|
"SUBTITLE": "Launch a WhatsApp campaign to reach your customers directly. Send offers or make announcements with ease. Click 'Create campaign' to get started."
|
||||||
|
},
|
||||||
|
"CARD": {
|
||||||
|
"STATUS": {
|
||||||
|
"COMPLETED": "Completed",
|
||||||
|
"SCHEDULED": "Scheduled"
|
||||||
|
},
|
||||||
|
"CAMPAIGN_DETAILS": {
|
||||||
|
"SENT_FROM": "Sent from",
|
||||||
|
"ON": "on"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CREATE": {
|
||||||
|
"TITLE": "Create WhatsApp campaign",
|
||||||
|
"CANCEL_BUTTON_TEXT": "Zrušit",
|
||||||
|
"CREATE_BUTTON_TEXT": "Create",
|
||||||
|
"FORM": {
|
||||||
|
"TITLE": {
|
||||||
|
"LABEL": "Title",
|
||||||
|
"PLACEHOLDER": "Please enter the title of campaign",
|
||||||
|
"ERROR": "Title is required"
|
||||||
|
},
|
||||||
|
"INBOX": {
|
||||||
|
"LABEL": "Select Inbox",
|
||||||
|
"PLACEHOLDER": "Select Inbox",
|
||||||
|
"ERROR": "Inbox is required"
|
||||||
|
},
|
||||||
|
"TEMPLATE": {
|
||||||
|
"LABEL": "WhatsApp Template",
|
||||||
|
"PLACEHOLDER": "Select a template",
|
||||||
|
"INFO": "Select a template to use for this campaign.",
|
||||||
|
"ERROR": "Template is required",
|
||||||
|
"PREVIEW_TITLE": "Process {templateName}",
|
||||||
|
"LANGUAGE": "Language",
|
||||||
|
"CATEGORY": "Category",
|
||||||
|
"VARIABLES_LABEL": "Variables",
|
||||||
|
"VARIABLE_PLACEHOLDER": "Enter value for {variable}"
|
||||||
|
},
|
||||||
|
"AUDIENCE": {
|
||||||
|
"LABEL": "Audience",
|
||||||
|
"PLACEHOLDER": "Select the customer labels",
|
||||||
|
"ERROR": "Audience is required"
|
||||||
|
},
|
||||||
|
"SCHEDULED_AT": {
|
||||||
|
"LABEL": "Scheduled time",
|
||||||
|
"PLACEHOLDER": "Please select the time",
|
||||||
|
"ERROR": "Scheduled time is required"
|
||||||
|
},
|
||||||
|
"BUTTONS": {
|
||||||
|
"CREATE": "Create",
|
||||||
|
"CANCEL": "Zrušit"
|
||||||
|
},
|
||||||
|
"API": {
|
||||||
|
"SUCCESS_MESSAGE": "WhatsApp campaign created successfully",
|
||||||
|
"ERROR_MESSAGE": "There was an error. Please try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"CONFIRM_DELETE": {
|
"CONFIRM_DELETE": {
|
||||||
"TITLE": "Opravdu chcete odstranit?",
|
"TITLE": "Opravdu chcete odstranit?",
|
||||||
"DESCRIPTION": "The delete action is permanent and cannot be reversed.",
|
"DESCRIPTION": "The delete action is permanent and cannot be reversed.",
|
||||||
|
|||||||
@@ -222,10 +222,17 @@
|
|||||||
"DESC": "Start supporting your customers via WhatsApp.",
|
"DESC": "Start supporting your customers via WhatsApp.",
|
||||||
"PROVIDERS": {
|
"PROVIDERS": {
|
||||||
"LABEL": "API Provider",
|
"LABEL": "API Provider",
|
||||||
|
"WHATSAPP_EMBEDDED": "WhatsApp Business",
|
||||||
"TWILIO": "Twilio",
|
"TWILIO": "Twilio",
|
||||||
"WHATSAPP_CLOUD": "WhatsApp Cloud",
|
"WHATSAPP_CLOUD": "WhatsApp Cloud",
|
||||||
|
"WHATSAPP_CLOUD_DESC": "Quick setup through Meta",
|
||||||
|
"TWILIO_DESC": "Connect via Twilio credentials",
|
||||||
"360_DIALOG": "360Dialog"
|
"360_DIALOG": "360Dialog"
|
||||||
},
|
},
|
||||||
|
"SELECT_PROVIDER": {
|
||||||
|
"TITLE": "Select your API provider",
|
||||||
|
"DESCRIPTION": "Choose your WhatsApp provider. You can connect directly through Meta which requires no setup, or connect through Twilio using your account credentials."
|
||||||
|
},
|
||||||
"INBOX_NAME": {
|
"INBOX_NAME": {
|
||||||
"LABEL": "Název schránky",
|
"LABEL": "Název schránky",
|
||||||
"PLACEHOLDER": "Please enter an inbox name",
|
"PLACEHOLDER": "Please enter an inbox name",
|
||||||
@@ -264,6 +271,28 @@
|
|||||||
"WEBHOOK_VERIFICATION_TOKEN": "Webhook Verification Token"
|
"WEBHOOK_VERIFICATION_TOKEN": "Webhook Verification Token"
|
||||||
},
|
},
|
||||||
"SUBMIT_BUTTON": "Create WhatsApp Channel",
|
"SUBMIT_BUTTON": "Create WhatsApp Channel",
|
||||||
|
"EMBEDDED_SIGNUP": {
|
||||||
|
"TITLE": "Quick Setup with Meta",
|
||||||
|
"DESC": "You will be redirected to Meta to log into your WhatsApp Business account. Having admin access will help make the setup smooth and easy.",
|
||||||
|
"BENEFITS": {
|
||||||
|
"TITLE": "Benefits of Embedded Signup:",
|
||||||
|
"EASY_SETUP": "No manual configuration required",
|
||||||
|
"SECURE_AUTH": "Secure OAuth based authentication",
|
||||||
|
"AUTO_CONFIG": "Automatic webhook and phone number configuration"
|
||||||
|
},
|
||||||
|
"SUBMIT_BUTTON": "Connect with WhatsApp Business",
|
||||||
|
"AUTH_PROCESSING": "Authenticating with Meta",
|
||||||
|
"WAITING_FOR_BUSINESS_INFO": "Please complete business setup in the Meta window...",
|
||||||
|
"PROCESSING": "Setting up your WhatsApp Business Account",
|
||||||
|
"LOADING_SDK": "Loading Facebook SDK...",
|
||||||
|
"CANCELLED": "WhatsApp Signup was cancelled",
|
||||||
|
"SUCCESS_TITLE": "WhatsApp Business Account Connected!",
|
||||||
|
"WAITING_FOR_AUTH": "Waiting for authentication...",
|
||||||
|
"INVALID_BUSINESS_DATA": "Invalid business data received from Facebook. Please try again.",
|
||||||
|
"SIGNUP_ERROR": "Signup error occurred",
|
||||||
|
"AUTH_NOT_COMPLETED": "Authentication not completed. Please restart the process.",
|
||||||
|
"SUCCESS_FALLBACK": "WhatsApp Business Account has been successfully configured"
|
||||||
|
},
|
||||||
"API": {
|
"API": {
|
||||||
"ERROR_MESSAGE": "We were not able to save the WhatsApp channel"
|
"ERROR_MESSAGE": "We were not able to save the WhatsApp channel"
|
||||||
}
|
}
|
||||||
@@ -571,6 +600,10 @@
|
|||||||
"WHATSAPP_SECTION_UPDATE_BUTTON": "Aktualizovat",
|
"WHATSAPP_SECTION_UPDATE_BUTTON": "Aktualizovat",
|
||||||
"WHATSAPP_WEBHOOK_TITLE": "Webhook Verification Token",
|
"WHATSAPP_WEBHOOK_TITLE": "Webhook Verification Token",
|
||||||
"WHATSAPP_WEBHOOK_SUBHEADER": "This token is used to verify the authenticity of the webhook endpoint.",
|
"WHATSAPP_WEBHOOK_SUBHEADER": "This token is used to verify the authenticity of the webhook endpoint.",
|
||||||
|
"WHATSAPP_TEMPLATES_SYNC_TITLE": "Sync Templates",
|
||||||
|
"WHATSAPP_TEMPLATES_SYNC_SUBHEADER": "Manually sync message templates from WhatsApp to update your available templates.",
|
||||||
|
"WHATSAPP_TEMPLATES_SYNC_BUTTON": "Sync Templates",
|
||||||
|
"WHATSAPP_TEMPLATES_SYNC_SUCCESS": "Templates sync initiated successfully. It may take a couple of minutes to update.",
|
||||||
"UPDATE_PRE_CHAT_FORM_SETTINGS": "Update Pre Chat Form Settings"
|
"UPDATE_PRE_CHAT_FORM_SETTINGS": "Update Pre Chat Form Settings"
|
||||||
},
|
},
|
||||||
"HELP_CENTER": {
|
"HELP_CENTER": {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user