Compare commits
166
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 | ||
|
|
10673a8d68 | ||
|
|
d488b8ced6 | ||
|
|
0381bdac96 | ||
|
|
87fee33039 | ||
|
|
fe68542547 | ||
|
|
2e1529c755 | ||
|
|
eb20e39b11 | ||
|
|
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,7 +179,8 @@ gem 'reverse_markdown'
|
|||||||
|
|
||||||
gem 'iso-639'
|
gem 'iso-639'
|
||||||
gem 'ruby-openai'
|
gem 'ruby-openai'
|
||||||
gem 'ai-agents', '>= 0.2.1'
|
gem 'ai-agents', '>= 0.4.3'
|
||||||
|
gem 'ruby_llm-schema'
|
||||||
|
|
||||||
gem 'shopify_api'
|
gem 'shopify_api'
|
||||||
|
|
||||||
|
|||||||
+16
-9
@@ -126,7 +126,7 @@ 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.2.1)
|
ai-agents (0.4.3)
|
||||||
ruby_llm (~> 1.3)
|
ruby_llm (~> 1.3)
|
||||||
annotate (3.2.0)
|
annotate (3.2.0)
|
||||||
activerecord (>= 3.2, < 8.0)
|
activerecord (>= 3.2, < 8.0)
|
||||||
@@ -359,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)
|
||||||
@@ -477,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
|
||||||
@@ -508,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)
|
||||||
@@ -719,7 +720,7 @@ 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.3.1)
|
ruby_llm (1.5.1)
|
||||||
base64
|
base64
|
||||||
event_stream_parser (~> 1)
|
event_stream_parser (~> 1)
|
||||||
faraday (>= 1.10.0)
|
faraday (>= 1.10.0)
|
||||||
@@ -728,6 +729,7 @@ GEM
|
|||||||
faraday-retry (>= 1)
|
faraday-retry (>= 1)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
zeitwerk (~> 2)
|
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)
|
||||||
@@ -785,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)
|
||||||
@@ -823,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
|
||||||
@@ -906,7 +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.2.1)
|
ai-agents (>= 0.4.3)
|
||||||
annotate
|
annotate
|
||||||
attr_extras
|
attr_extras
|
||||||
audited (~> 5.4, >= 5.4.1)
|
audited (~> 5.4, >= 5.4.1)
|
||||||
@@ -1000,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
|
||||||
@@ -1010,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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { useToggle } from '@vueuse/core';
|
import { useToggle } from '@vueuse/core';
|
||||||
|
import { vOnClickOutside } from '@vueuse/components';
|
||||||
|
|
||||||
import Button from 'dashboard/components-next/button/Button.vue';
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
import InlineInput from 'dashboard/components-next/inline-input/InlineInput.vue';
|
import InlineInput from 'dashboard/components-next/inline-input/InlineInput.vue';
|
||||||
@@ -44,7 +45,10 @@ const onClickCancel = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="inline-flex relative">
|
<div
|
||||||
|
v-on-click-outside="() => togglePopover(false)"
|
||||||
|
class="inline-flex relative"
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
:label="buttonLabel"
|
:label="buttonLabel"
|
||||||
sm
|
sm
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -73,7 +73,6 @@ const saveEdit = () => {
|
|||||||
v-if="isEditing"
|
v-if="isEditing"
|
||||||
v-model="editedContent"
|
v-model="editedContent"
|
||||||
focus-on-mount
|
focus-on-mount
|
||||||
custom-input-class="flex items-center gap-2 text-sm text-n-slate-12"
|
|
||||||
@keyup.enter="saveEdit"
|
@keyup.enter="saveEdit"
|
||||||
/>
|
/>
|
||||||
<span v-else class="flex items-center gap-2 text-sm text-n-slate-12">
|
<span v-else class="flex items-center gap-2 text-sm text-n-slate-12">
|
||||||
|
|||||||
@@ -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,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,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -600,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": {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -600,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": {
|
||||||
|
|||||||
@@ -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": "نص القالب",
|
||||||
|
|||||||
@@ -600,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": {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -600,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": {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -600,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": {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -600,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": {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -600,6 +600,10 @@
|
|||||||
"WHATSAPP_SECTION_UPDATE_BUTTON": "Opdater",
|
"WHATSAPP_SECTION_UPDATE_BUTTON": "Opdater",
|
||||||
"WHATSAPP_WEBHOOK_TITLE": "Webhook verifikations token",
|
"WHATSAPP_WEBHOOK_TITLE": "Webhook verifikations 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": {
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
"PICKER": {
|
"PICKER": {
|
||||||
"SEARCH_PLACEHOLDER": "Søg Skabeloner",
|
"SEARCH_PLACEHOLDER": "Søg Skabeloner",
|
||||||
"NO_TEMPLATES_FOUND": "Ingen skabeloner fundet for",
|
"NO_TEMPLATES_FOUND": "Ingen skabeloner fundet 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": "Sprog",
|
"LANGUAGE": "Sprog",
|
||||||
"TEMPLATE_BODY": "Skabelon Krop",
|
"TEMPLATE_BODY": "Skabelon Krop",
|
||||||
|
|||||||
@@ -600,6 +600,10 @@
|
|||||||
"WHATSAPP_SECTION_UPDATE_BUTTON": "Aktualisieren",
|
"WHATSAPP_SECTION_UPDATE_BUTTON": "Aktualisieren",
|
||||||
"WHATSAPP_WEBHOOK_TITLE": "Webhook-Verifizierungstoken",
|
"WHATSAPP_WEBHOOK_TITLE": "Webhook-Verifizierungstoken",
|
||||||
"WHATSAPP_WEBHOOK_SUBHEADER": "Mit diesem Token wird die Authentizität des Webhook Endpunktes überprüft.",
|
"WHATSAPP_WEBHOOK_SUBHEADER": "Mit diesem Token wird die Authentizität des Webhook Endpunktes überprüft.",
|
||||||
|
"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": "Pre Chat Einstellungen aktualisieren"
|
"UPDATE_PRE_CHAT_FORM_SETTINGS": "Pre Chat Einstellungen aktualisieren"
|
||||||
},
|
},
|
||||||
"HELP_CENTER": {
|
"HELP_CENTER": {
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
"PICKER": {
|
"PICKER": {
|
||||||
"SEARCH_PLACEHOLDER": "Vorlagen suchen",
|
"SEARCH_PLACEHOLDER": "Vorlagen suchen",
|
||||||
"NO_TEMPLATES_FOUND": "Keine Vorlagen gefunden für",
|
"NO_TEMPLATES_FOUND": "Keine Vorlagen gefunden für",
|
||||||
|
"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": "Sprache",
|
"LANGUAGE": "Sprache",
|
||||||
"TEMPLATE_BODY": "Vorlagenbody",
|
"TEMPLATE_BODY": "Vorlagenbody",
|
||||||
|
|||||||
@@ -600,6 +600,10 @@
|
|||||||
"WHATSAPP_SECTION_UPDATE_BUTTON": "Ενημέρωση",
|
"WHATSAPP_SECTION_UPDATE_BUTTON": "Ενημέρωση",
|
||||||
"WHATSAPP_WEBHOOK_TITLE": "Token Επαλήθευσης Webhook",
|
"WHATSAPP_WEBHOOK_TITLE": "Token Επαλήθευσης 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": {
|
||||||
|
|||||||
@@ -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": "Σώμα Προτύπου",
|
||||||
|
|||||||
@@ -144,6 +144,9 @@
|
|||||||
"AGENTS_LOADING": "Loading agents...",
|
"AGENTS_LOADING": "Loading agents...",
|
||||||
"ASSIGN_TEAM": "Assign team",
|
"ASSIGN_TEAM": "Assign team",
|
||||||
"DELETE": "Delete conversation",
|
"DELETE": "Delete conversation",
|
||||||
|
"OPEN_IN_NEW_TAB": "Open in new tab",
|
||||||
|
"COPY_LINK": "Copy conversation link",
|
||||||
|
"COPY_LINK_SUCCESS": "Conversation link copied to clipboard",
|
||||||
"API": {
|
"API": {
|
||||||
"AGENT_ASSIGNMENT": {
|
"AGENT_ASSIGNMENT": {
|
||||||
"SUCCESFUL": "Conversation id {conversationId} assigned to \"{agentName}\"",
|
"SUCCESFUL": "Conversation id {conversationId} assigned to \"{agentName}\"",
|
||||||
|
|||||||
@@ -157,6 +157,12 @@
|
|||||||
"DELETE_SUCCESS": "Portal deleted successfully",
|
"DELETE_SUCCESS": "Portal deleted successfully",
|
||||||
"DELETE_ERROR": "Error while deleting portal"
|
"DELETE_ERROR": "Error while deleting portal"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"SEND_CNAME_INSTRUCTIONS": {
|
||||||
|
"API": {
|
||||||
|
"SUCCESS_MESSAGE": "CNAME instructions sent successfully",
|
||||||
|
"ERROR_MESSAGE": "Error while sending CNAME instructions"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"EDIT": {
|
"EDIT": {
|
||||||
@@ -747,9 +753,15 @@
|
|||||||
"HEADER": "Custom domain",
|
"HEADER": "Custom domain",
|
||||||
"LABEL": "Custom domain:",
|
"LABEL": "Custom domain:",
|
||||||
"DESCRIPTION": "You can host your portal on a custom domain. For instance, if your website is yourdomain.com and you want your portal available at docs.yourdomain.com, simply enter that in this field.",
|
"DESCRIPTION": "You can host your portal on a custom domain. For instance, if your website is yourdomain.com and you want your portal available at docs.yourdomain.com, simply enter that in this field.",
|
||||||
|
"STATUS_DESCRIPTION": "Your custom portal will start working as soon as it is verified.",
|
||||||
"PLACEHOLDER": "Portal custom domain",
|
"PLACEHOLDER": "Portal custom domain",
|
||||||
"EDIT_BUTTON": "Edit custom domain",
|
"EDIT_BUTTON": "Edit",
|
||||||
"ADD_BUTTON": "Add custom domain",
|
"ADD_BUTTON": "Add custom domain",
|
||||||
|
"STATUS": {
|
||||||
|
"LIVE": "Live",
|
||||||
|
"PENDING": "Awaiting verification",
|
||||||
|
"ERROR": "Verification failed"
|
||||||
|
},
|
||||||
"DIALOG": {
|
"DIALOG": {
|
||||||
"ADD_HEADER": "Add custom domain",
|
"ADD_HEADER": "Add custom domain",
|
||||||
"EDIT_HEADER": "Edit custom domain",
|
"EDIT_HEADER": "Edit custom domain",
|
||||||
@@ -757,13 +769,20 @@
|
|||||||
"EDIT_CONFIRM_BUTTON_LABEL": "Update domain",
|
"EDIT_CONFIRM_BUTTON_LABEL": "Update domain",
|
||||||
"LABEL": "Custom domain",
|
"LABEL": "Custom domain",
|
||||||
"PLACEHOLDER": "Portal custom domain",
|
"PLACEHOLDER": "Portal custom domain",
|
||||||
"ERROR": "Custom domain is required"
|
"ERROR": "Custom domain is required",
|
||||||
|
"FORMAT_ERROR": "Please enter a valid domain URL e.g. docs.yourdomain.com"
|
||||||
},
|
},
|
||||||
"DNS_CONFIGURATION_DIALOG": {
|
"DNS_CONFIGURATION_DIALOG": {
|
||||||
"HEADER": "DNS configuration",
|
"HEADER": "DNS configuration",
|
||||||
"DESCRIPTION": "Log in to the account you have with your DNS provider, and add a CNAME record for subdomain pointing to chatwoot.help",
|
"DESCRIPTION": "Log in to the account you have with your DNS provider, and add a CNAME record for subdomain pointing to chatwoot.help",
|
||||||
"HELP_TEXT": "Once this is done, you can reach out to our support to request for the auto-generated SSL certificate.",
|
"COPY": "Successfully copied CNAME",
|
||||||
"CONFIRM_BUTTON_LABEL": "Got it!"
|
"SEND_INSTRUCTIONS": {
|
||||||
|
"HEADER": "Send instructions",
|
||||||
|
"DESCRIPTION": "If you would prefer to have someone from your development team to handle this step, you can enter email address below, and we will send them the required instructions.",
|
||||||
|
"PLACEHOLDER": "Enter their email",
|
||||||
|
"ERROR": "Enter a valid email address",
|
||||||
|
"SEND_BUTTON": "Send"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"DELETE_PORTAL": {
|
"DELETE_PORTAL": {
|
||||||
|
|||||||
@@ -600,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": {
|
||||||
|
|||||||
@@ -554,6 +554,7 @@
|
|||||||
"SEARCH_PLACEHOLDER": "Search..."
|
"SEARCH_PLACEHOLDER": "Search..."
|
||||||
},
|
},
|
||||||
"EMPTY_MESSAGE": "No guardrails found. Create or add examples to begin.",
|
"EMPTY_MESSAGE": "No guardrails found. Create or add examples to begin.",
|
||||||
|
"SEARCH_EMPTY_MESSAGE": "No guardrails found for this search.",
|
||||||
"API": {
|
"API": {
|
||||||
"ADD": {
|
"ADD": {
|
||||||
"SUCCESS": "Guardrails added successfully",
|
"SUCCESS": "Guardrails added successfully",
|
||||||
@@ -601,6 +602,7 @@
|
|||||||
"SEARCH_PLACEHOLDER": "Search..."
|
"SEARCH_PLACEHOLDER": "Search..."
|
||||||
},
|
},
|
||||||
"EMPTY_MESSAGE": "No response guidelines found. Create or add examples to begin.",
|
"EMPTY_MESSAGE": "No response guidelines found. Create or add examples to begin.",
|
||||||
|
"SEARCH_EMPTY_MESSAGE": "No response guidelines found for this search.",
|
||||||
"API": {
|
"API": {
|
||||||
"ADD": {
|
"ADD": {
|
||||||
"SUCCESS": "Response Guidelines added successfully",
|
"SUCCESS": "Response Guidelines added successfully",
|
||||||
@@ -615,6 +617,73 @@
|
|||||||
"ERROR": "There was an error deleting response guidelines, please try again."
|
"ERROR": "There was an error deleting response guidelines, please try again."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"SCENARIOS": {
|
||||||
|
"TITLE": "Scenarios",
|
||||||
|
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”",
|
||||||
|
"BREADCRUMB": {
|
||||||
|
"TITLE": "Scenarios"
|
||||||
|
},
|
||||||
|
"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 scenarios",
|
||||||
|
"ADD": "Add all",
|
||||||
|
"ADD_SINGLE": "Add this",
|
||||||
|
"TOOLS_USED": "Tools used :"
|
||||||
|
},
|
||||||
|
"NEW": {
|
||||||
|
"CREATE": "Add a scenario",
|
||||||
|
"TITLE": "Create a scenario",
|
||||||
|
"FORM": {
|
||||||
|
"TITLE": {
|
||||||
|
"LABEL": "Title",
|
||||||
|
"PLACEHOLDER": "Enter a name for the scenario",
|
||||||
|
"ERROR": "Scenario name is required"
|
||||||
|
},
|
||||||
|
"DESCRIPTION": {
|
||||||
|
"LABEL": "Description",
|
||||||
|
"PLACEHOLDER": "Describe how and where this scenario will be used",
|
||||||
|
"ERROR": "Scenario description is required"
|
||||||
|
},
|
||||||
|
"INSTRUCTION": {
|
||||||
|
"LABEL": "How to handle",
|
||||||
|
"PLACEHOLDER": "Describe how and where this scenario will be handled",
|
||||||
|
"ERROR": "Scenario content is required"
|
||||||
|
},
|
||||||
|
"CREATE": "Create",
|
||||||
|
"CANCEL": "Cancel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"UPDATE": {
|
||||||
|
"CANCEL": "Cancel",
|
||||||
|
"UPDATE": "Update changes"
|
||||||
|
},
|
||||||
|
"LIST": {
|
||||||
|
"SEARCH_PLACEHOLDER": "Search..."
|
||||||
|
},
|
||||||
|
"EMPTY_MESSAGE": "No scenarios found. Create or add examples to begin.",
|
||||||
|
"SEARCH_EMPTY_MESSAGE": "No scenarios found for this search.",
|
||||||
|
"API": {
|
||||||
|
"ADD": {
|
||||||
|
"SUCCESS": "Scenarios added successfully",
|
||||||
|
"ERROR": "There was an error adding scenarios, please try again."
|
||||||
|
},
|
||||||
|
"UPDATE": {
|
||||||
|
"SUCCESS": "Scenarios updated successfully",
|
||||||
|
"ERROR": "There was an error updating scenarios, please try again."
|
||||||
|
},
|
||||||
|
"DELETE": {
|
||||||
|
"SUCCESS": "Scenarios deleted successfully",
|
||||||
|
"ERROR": "There was an error deleting scenarios, please try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"DOCUMENTS": {
|
"DOCUMENTS": {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -600,6 +600,10 @@
|
|||||||
"WHATSAPP_SECTION_UPDATE_BUTTON": "Actualizar",
|
"WHATSAPP_SECTION_UPDATE_BUTTON": "Actualizar",
|
||||||
"WHATSAPP_WEBHOOK_TITLE": "Token de verificación del Webhook",
|
"WHATSAPP_WEBHOOK_TITLE": "Token de verificación del Webhook",
|
||||||
"WHATSAPP_WEBHOOK_SUBHEADER": "Este token se utiliza para verificar la autenticidad del extremo del webhook.",
|
"WHATSAPP_WEBHOOK_SUBHEADER": "Este token se utiliza para verificar la autenticidad del extremo 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": "Actualizar configuración de Formulario de Chat"
|
"UPDATE_PRE_CHAT_FORM_SETTINGS": "Actualizar configuración de Formulario de Chat"
|
||||||
},
|
},
|
||||||
"HELP_CENTER": {
|
"HELP_CENTER": {
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
"PICKER": {
|
"PICKER": {
|
||||||
"SEARCH_PLACEHOLDER": "Buscar plantillas",
|
"SEARCH_PLACEHOLDER": "Buscar plantillas",
|
||||||
"NO_TEMPLATES_FOUND": "No se encontraron plantillas para",
|
"NO_TEMPLATES_FOUND": "No se encontraron plantillas para",
|
||||||
|
"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": "Cuerpo de plantilla",
|
"TEMPLATE_BODY": "Cuerpo de plantilla",
|
||||||
|
|||||||
@@ -600,6 +600,10 @@
|
|||||||
"WHATSAPP_SECTION_UPDATE_BUTTON": "اعمال شود",
|
"WHATSAPP_SECTION_UPDATE_BUTTON": "اعمال شود",
|
||||||
"WHATSAPP_WEBHOOK_TITLE": "توکن تایید Webhook",
|
"WHATSAPP_WEBHOOK_TITLE": "توکن تایید Webhook",
|
||||||
"WHATSAPP_WEBHOOK_SUBHEADER": "این توکن برای تأیید صحت نقطه پایانی webhook استفاده می شود.",
|
"WHATSAPP_WEBHOOK_SUBHEADER": "این توکن برای تأیید صحت نقطه پایانی 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": "بهروزرسانی تنظیمات فرم قبل از گفتگو"
|
"UPDATE_PRE_CHAT_FORM_SETTINGS": "بهروزرسانی تنظیمات فرم قبل از گفتگو"
|
||||||
},
|
},
|
||||||
"HELP_CENTER": {
|
"HELP_CENTER": {
|
||||||
|
|||||||
@@ -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": "بدنه الگو",
|
||||||
|
|||||||
@@ -600,6 +600,10 @@
|
|||||||
"WHATSAPP_SECTION_UPDATE_BUTTON": "Päivitä",
|
"WHATSAPP_SECTION_UPDATE_BUTTON": "Päivitä",
|
||||||
"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": {
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
"PICKER": {
|
"PICKER": {
|
||||||
"SEARCH_PLACEHOLDER": "Etsi Pohjia",
|
"SEARCH_PLACEHOLDER": "Etsi Pohjia",
|
||||||
"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",
|
||||||
|
|||||||
@@ -600,6 +600,10 @@
|
|||||||
"WHATSAPP_SECTION_UPDATE_BUTTON": "Mettre à jour",
|
"WHATSAPP_SECTION_UPDATE_BUTTON": "Mettre à jour",
|
||||||
"WHATSAPP_WEBHOOK_TITLE": "Jeton de vérification du Webhook",
|
"WHATSAPP_WEBHOOK_TITLE": "Jeton de vérification du Webhook",
|
||||||
"WHATSAPP_WEBHOOK_SUBHEADER": "Ce jeton est utilisé pour vérifier l'authenticité du point de terminaison du webhook.",
|
"WHATSAPP_WEBHOOK_SUBHEADER": "Ce jeton est utilisé pour vérifier l'authenticité du point de terminaison du 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": "Mettre à jour les paramètres du formulaire de pré-chat"
|
"UPDATE_PRE_CHAT_FORM_SETTINGS": "Mettre à jour les paramètres du formulaire de pré-chat"
|
||||||
},
|
},
|
||||||
"HELP_CENTER": {
|
"HELP_CENTER": {
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
"PICKER": {
|
"PICKER": {
|
||||||
"SEARCH_PLACEHOLDER": "Rechercher des modèles",
|
"SEARCH_PLACEHOLDER": "Rechercher des modèles",
|
||||||
"NO_TEMPLATES_FOUND": "Aucun modèle trouvé pour",
|
"NO_TEMPLATES_FOUND": "Aucun modèle trouvé pour",
|
||||||
|
"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": "Langue",
|
"LANGUAGE": "Langue",
|
||||||
"TEMPLATE_BODY": "Corps du modèle",
|
"TEMPLATE_BODY": "Corps du modèle",
|
||||||
|
|||||||
@@ -600,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": "עדכון הגדרות טופס טרום צ'אט"
|
||||||
},
|
},
|
||||||
"HELP_CENTER": {
|
"HELP_CENTER": {
|
||||||
|
|||||||
@@ -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": "גוף התבנית",
|
||||||
|
|||||||
@@ -600,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": {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -600,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": "Ovaj token se koristi za verifikaciju autentičnosti webhook endpoint-a.",
|
"WHATSAPP_WEBHOOK_SUBHEADER": "Ovaj token se koristi za verifikaciju autentičnosti webhook endpoint-a.",
|
||||||
|
"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": {
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
"PICKER": {
|
"PICKER": {
|
||||||
"SEARCH_PLACEHOLDER": "Pretraži Predloške",
|
"SEARCH_PLACEHOLDER": "Pretraži Predloške",
|
||||||
"NO_TEMPLATES_FOUND": "Nije pronađen predložak za",
|
"NO_TEMPLATES_FOUND": "Nije pronađen predložak za",
|
||||||
|
"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": "Jezik",
|
"LANGUAGE": "Jezik",
|
||||||
"TEMPLATE_BODY": "Tijelo predloška",
|
"TEMPLATE_BODY": "Tijelo predloška",
|
||||||
|
|||||||
@@ -600,6 +600,10 @@
|
|||||||
"WHATSAPP_SECTION_UPDATE_BUTTON": "Frissítés",
|
"WHATSAPP_SECTION_UPDATE_BUTTON": "Frissítés",
|
||||||
"WHATSAPP_WEBHOOK_TITLE": "Webhook Verification Token",
|
"WHATSAPP_WEBHOOK_TITLE": "Webhook Verification Token",
|
||||||
"WHATSAPP_WEBHOOK_SUBHEADER": "Ez a token a webhook-végpont hitelességének ellenőrzésére szolgál.",
|
"WHATSAPP_WEBHOOK_SUBHEADER": "Ez a token a webhook-végpont hitelességének ellenőrzésére szolgál.",
|
||||||
|
"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": "Csevegés előtti űrlap beállításainak frissítése"
|
"UPDATE_PRE_CHAT_FORM_SETTINGS": "Csevegés előtti űrlap beállításainak frissítése"
|
||||||
},
|
},
|
||||||
"HELP_CENTER": {
|
"HELP_CENTER": {
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
"PICKER": {
|
"PICKER": {
|
||||||
"SEARCH_PLACEHOLDER": "Sablon keresése",
|
"SEARCH_PLACEHOLDER": "Sablon keresése",
|
||||||
"NO_TEMPLATES_FOUND": "Nem található sablon erre:",
|
"NO_TEMPLATES_FOUND": "Nem található sablon erre:",
|
||||||
|
"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": "Nyelv",
|
"LANGUAGE": "Nyelv",
|
||||||
"TEMPLATE_BODY": "Sablon törzse",
|
"TEMPLATE_BODY": "Sablon törzse",
|
||||||
|
|||||||
@@ -600,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": {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -600,6 +600,10 @@
|
|||||||
"WHATSAPP_SECTION_UPDATE_BUTTON": "Perbarui",
|
"WHATSAPP_SECTION_UPDATE_BUTTON": "Perbarui",
|
||||||
"WHATSAPP_WEBHOOK_TITLE": "Token Verifikasi Webhook",
|
"WHATSAPP_WEBHOOK_TITLE": "Token Verifikasi Webhook",
|
||||||
"WHATSAPP_WEBHOOK_SUBHEADER": "Token ini digunakan untuk memverifikasi keaslian titik akhir webhook.",
|
"WHATSAPP_WEBHOOK_SUBHEADER": "Token ini digunakan untuk memverifikasi keaslian titik akhir 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": "Perbarui Pengaturan Formulir Pra Obrolan"
|
"UPDATE_PRE_CHAT_FORM_SETTINGS": "Perbarui Pengaturan Formulir Pra Obrolan"
|
||||||
},
|
},
|
||||||
"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