Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
676493eff2 | ||
|
|
b0d8eb3a2b | ||
|
|
655db56be9 | ||
|
|
7f56cd92f8 | ||
|
|
c07529c2b0 | ||
|
|
d48503bdcf | ||
|
|
d35b0a9465 | ||
|
|
83873d49ae | ||
|
|
6ca38e10e9 | ||
|
|
1a1dfd09cb | ||
|
|
35d0a7f1a7 | ||
|
|
47867c0b8a | ||
|
|
530125d4c5 | ||
|
|
7679004bb4 | ||
|
|
ae3ac33049 | ||
|
|
714f24de11 | ||
|
|
3038e672f8 | ||
|
|
7aad3c9c6b | ||
|
|
c6113852d7 | ||
|
|
94e930a141 | ||
|
|
7fe94dc1a2 | ||
|
|
cae097c5fa | ||
|
|
faf35738b3 | ||
|
|
341487b93e | ||
|
|
f569ed93ad | ||
|
|
eac292b11b | ||
|
|
75119d4a08 | ||
|
|
67d7ee0185 | ||
|
|
7e33e033cd | ||
|
|
02fa76f3df | ||
|
|
91d80004a6 | ||
|
|
b809cd2f15 | ||
|
|
ee773f31eb | ||
|
|
1f03fc4dc3 | ||
|
|
a42b99ada0 | ||
|
|
c6be04cdc1 | ||
|
|
14471cc20c | ||
|
|
6b42ff8d39 | ||
|
|
a88fef2e1d | ||
|
|
ee9f1d7adb | ||
|
|
b711bfd2ca | ||
|
|
42af4b1d01 | ||
|
|
9a7318a9db | ||
|
|
48fa7bf72b | ||
|
|
469e724e3a | ||
|
|
0c101b1f6b | ||
|
|
ad4ec9e93b | ||
|
|
5c560c7628 | ||
|
|
dbb164a37d | ||
|
|
fdcfed2cd7 |
@@ -94,3 +94,4 @@ yarn-debug.log*
|
||||
.vscode
|
||||
.claude/settings.local.json
|
||||
.cursor
|
||||
CLAUDE.local.md
|
||||
|
||||
@@ -55,4 +55,21 @@
|
||||
|
||||
## Ruby Best Practices
|
||||
|
||||
- Use compact `module/class` definitions; avoid nested styles
|
||||
- Use compact `module/class` definitions; avoid nested styles
|
||||
|
||||
## Enterprise Edition Notes
|
||||
|
||||
- Chatwoot has an Enterprise overlay under `enterprise/` that extends/overrides OSS code.
|
||||
- When you add or modify core functionality, always check for corresponding files in `enterprise/` and keep behavior compatible.
|
||||
- Follow the Enterprise development practices documented here:
|
||||
- https://chatwoot.help/hc/handbook/articles/developing-enterprise-edition-features-38
|
||||
|
||||
Practical checklist for any change impacting core logic or public APIs
|
||||
- Search for related files in both trees before editing (e.g., `rg -n "FooService|ControllerName|ModelName" app enterprise`).
|
||||
- If adding new endpoints, services, or models, consider whether Enterprise needs:
|
||||
- An override (e.g., `enterprise/app/...`), or
|
||||
- An extension point (e.g., `prepend_mod_with`, hooks, configuration) to avoid hard forks.
|
||||
- Avoid hardcoding instance- or plan-specific behavior in OSS; prefer configuration, feature flags, or extension points consumed by Enterprise.
|
||||
- Keep request/response contracts stable across OSS and Enterprise; update both sets of routes/controllers when introducing new APIs.
|
||||
- When renaming/moving shared code, mirror the change in `enterprise/` to prevent drift.
|
||||
- Tests: Add Enterprise-specific specs under `spec/enterprise`, mirroring OSS spec layout where applicable.
|
||||
|
||||
@@ -89,7 +89,7 @@ gem 'wisper', '2.0.0'
|
||||
##--- gems for channels ---##
|
||||
gem 'facebook-messenger'
|
||||
gem 'line-bot-api'
|
||||
gem 'twilio-ruby', '~> 5.66'
|
||||
gem 'twilio-ruby'
|
||||
# twitty will handle subscription of twitter account events
|
||||
# gem 'twitty', git: 'https://github.com/chatwoot/twitty'
|
||||
gem 'twitty', '~> 0.1.5'
|
||||
@@ -108,7 +108,7 @@ gem 'google-cloud-translate-v3', '>= 0.7.0'
|
||||
##-- apm and error monitoring ---#
|
||||
# loaded only when environment variables are set.
|
||||
# ref application.rb
|
||||
gem 'ddtrace', require: false
|
||||
gem 'datadog', '~> 2.0', require: false
|
||||
gem 'elastic-apm', require: false
|
||||
gem 'newrelic_rpm', require: false
|
||||
gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false
|
||||
@@ -179,7 +179,10 @@ gem 'reverse_markdown'
|
||||
|
||||
gem 'iso-639'
|
||||
gem 'ruby-openai'
|
||||
gem 'ai-agents', '>= 0.2.1'
|
||||
gem 'ai-agents', '>= 0.4.3'
|
||||
|
||||
# TODO: Move this gem as a dependency of ai-agents
|
||||
gem 'ruby_llm-schema'
|
||||
|
||||
gem 'shopify_api'
|
||||
|
||||
@@ -209,6 +212,8 @@ group :development do
|
||||
gem 'stackprof'
|
||||
# Should install the associated chrome extension to view query logs
|
||||
gem 'meta_request', '>= 0.8.3'
|
||||
|
||||
gem 'tidewave'
|
||||
end
|
||||
|
||||
group :test do
|
||||
|
||||
+139
-80
@@ -25,35 +25,35 @@ GIT
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (7.1.5.1)
|
||||
actionpack (= 7.1.5.1)
|
||||
activesupport (= 7.1.5.1)
|
||||
actioncable (7.1.5.2)
|
||||
actionpack (= 7.1.5.2)
|
||||
activesupport (= 7.1.5.2)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
zeitwerk (~> 2.6)
|
||||
actionmailbox (7.1.5.1)
|
||||
actionpack (= 7.1.5.1)
|
||||
activejob (= 7.1.5.1)
|
||||
activerecord (= 7.1.5.1)
|
||||
activestorage (= 7.1.5.1)
|
||||
activesupport (= 7.1.5.1)
|
||||
actionmailbox (7.1.5.2)
|
||||
actionpack (= 7.1.5.2)
|
||||
activejob (= 7.1.5.2)
|
||||
activerecord (= 7.1.5.2)
|
||||
activestorage (= 7.1.5.2)
|
||||
activesupport (= 7.1.5.2)
|
||||
mail (>= 2.7.1)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
actionmailer (7.1.5.1)
|
||||
actionpack (= 7.1.5.1)
|
||||
actionview (= 7.1.5.1)
|
||||
activejob (= 7.1.5.1)
|
||||
activesupport (= 7.1.5.1)
|
||||
actionmailer (7.1.5.2)
|
||||
actionpack (= 7.1.5.2)
|
||||
actionview (= 7.1.5.2)
|
||||
activejob (= 7.1.5.2)
|
||||
activesupport (= 7.1.5.2)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
rails-dom-testing (~> 2.2)
|
||||
actionpack (7.1.5.1)
|
||||
actionview (= 7.1.5.1)
|
||||
activesupport (= 7.1.5.1)
|
||||
actionpack (7.1.5.2)
|
||||
actionview (= 7.1.5.2)
|
||||
activesupport (= 7.1.5.2)
|
||||
nokogiri (>= 1.8.5)
|
||||
racc
|
||||
rack (>= 2.2.4)
|
||||
@@ -61,38 +61,38 @@ GEM
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
actiontext (7.1.5.1)
|
||||
actionpack (= 7.1.5.1)
|
||||
activerecord (= 7.1.5.1)
|
||||
activestorage (= 7.1.5.1)
|
||||
activesupport (= 7.1.5.1)
|
||||
actiontext (7.1.5.2)
|
||||
actionpack (= 7.1.5.2)
|
||||
activerecord (= 7.1.5.2)
|
||||
activestorage (= 7.1.5.2)
|
||||
activesupport (= 7.1.5.2)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (7.1.5.1)
|
||||
activesupport (= 7.1.5.1)
|
||||
actionview (7.1.5.2)
|
||||
activesupport (= 7.1.5.2)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
active_record_query_trace (1.8)
|
||||
activejob (7.1.5.1)
|
||||
activesupport (= 7.1.5.1)
|
||||
activejob (7.1.5.2)
|
||||
activesupport (= 7.1.5.2)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (7.1.5.1)
|
||||
activesupport (= 7.1.5.1)
|
||||
activerecord (7.1.5.1)
|
||||
activemodel (= 7.1.5.1)
|
||||
activesupport (= 7.1.5.1)
|
||||
activemodel (7.1.5.2)
|
||||
activesupport (= 7.1.5.2)
|
||||
activerecord (7.1.5.2)
|
||||
activemodel (= 7.1.5.2)
|
||||
activesupport (= 7.1.5.2)
|
||||
timeout (>= 0.4.0)
|
||||
activerecord-import (2.1.0)
|
||||
activerecord (>= 4.2)
|
||||
activestorage (7.1.5.1)
|
||||
actionpack (= 7.1.5.1)
|
||||
activejob (= 7.1.5.1)
|
||||
activerecord (= 7.1.5.1)
|
||||
activesupport (= 7.1.5.1)
|
||||
activestorage (7.1.5.2)
|
||||
actionpack (= 7.1.5.2)
|
||||
activejob (= 7.1.5.2)
|
||||
activerecord (= 7.1.5.2)
|
||||
activesupport (= 7.1.5.2)
|
||||
marcel (~> 1.0)
|
||||
activesupport (7.1.5.1)
|
||||
activesupport (7.1.5.2)
|
||||
base64
|
||||
benchmark (>= 0.3)
|
||||
bigdecimal
|
||||
@@ -126,7 +126,7 @@ GEM
|
||||
jbuilder (~> 2)
|
||||
rails (>= 4.2, < 7.2)
|
||||
selectize-rails (~> 0.6)
|
||||
ai-agents (0.2.1)
|
||||
ai-agents (0.4.3)
|
||||
ruby_llm (~> 1.3)
|
||||
annotate (3.2.0)
|
||||
activerecord (>= 3.2, < 8.0)
|
||||
@@ -155,10 +155,10 @@ GEM
|
||||
barnes (0.0.9)
|
||||
multi_json (~> 1)
|
||||
statsd-ruby (~> 1.1)
|
||||
base64 (0.2.0)
|
||||
base64 (0.3.0)
|
||||
bcrypt (3.1.20)
|
||||
benchmark (0.4.0)
|
||||
bigdecimal (3.1.9)
|
||||
benchmark (0.4.1)
|
||||
bigdecimal (3.2.2)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.16.0)
|
||||
msgpack (~> 1.2)
|
||||
@@ -194,10 +194,14 @@ GEM
|
||||
activerecord (>= 5.a)
|
||||
database_cleaner-core (~> 2.0.0)
|
||||
database_cleaner-core (2.0.1)
|
||||
date (3.4.1)
|
||||
ddtrace (0.48.0)
|
||||
ffi (~> 1.0)
|
||||
datadog (2.19.0)
|
||||
datadog-ruby_core_source (~> 3.4, >= 3.4.1)
|
||||
libdatadog (~> 18.1.0.1.0)
|
||||
libddwaf (~> 1.24.1.0.3)
|
||||
logger
|
||||
msgpack
|
||||
datadog-ruby_core_source (3.4.1)
|
||||
date (3.4.1)
|
||||
debug (1.8.0)
|
||||
irb (>= 1.5.0)
|
||||
reline (>= 0.3.1)
|
||||
@@ -226,6 +230,35 @@ GEM
|
||||
addressable (~> 2.8)
|
||||
drb (2.2.3)
|
||||
dry-cli (1.1.0)
|
||||
dry-configurable (1.3.0)
|
||||
dry-core (~> 1.1)
|
||||
zeitwerk (~> 2.6)
|
||||
dry-core (1.1.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
logger
|
||||
zeitwerk (~> 2.6)
|
||||
dry-inflector (1.2.0)
|
||||
dry-initializer (3.2.0)
|
||||
dry-logic (1.6.0)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0)
|
||||
dry-core (~> 1.1)
|
||||
zeitwerk (~> 2.6)
|
||||
dry-schema (1.14.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
dry-configurable (~> 1.0, >= 1.0.1)
|
||||
dry-core (~> 1.1)
|
||||
dry-initializer (~> 3.2)
|
||||
dry-logic (~> 1.5)
|
||||
dry-types (~> 1.8)
|
||||
zeitwerk (~> 2.6)
|
||||
dry-types (1.8.3)
|
||||
bigdecimal (~> 3.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
dry-core (~> 1.0)
|
||||
dry-inflector (~> 1.0)
|
||||
dry-logic (~> 1.4)
|
||||
zeitwerk (~> 2.6)
|
||||
ecma-re-validator (0.4.0)
|
||||
regexp_parser (~> 2.2)
|
||||
elastic-apm (4.6.2)
|
||||
@@ -248,8 +281,10 @@ GEM
|
||||
railties (>= 5.0.0)
|
||||
faker (3.2.0)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (2.9.0)
|
||||
faraday-net_http (>= 2.0, < 3.2)
|
||||
faraday (2.13.1)
|
||||
faraday-net_http (>= 2.0, < 3.5)
|
||||
json
|
||||
logger
|
||||
faraday-follow_redirects (0.3.0)
|
||||
faraday (>= 1, < 3)
|
||||
faraday-mashify (0.1.1)
|
||||
@@ -257,13 +292,20 @@ GEM
|
||||
hashie
|
||||
faraday-multipart (1.0.4)
|
||||
multipart-post (~> 2)
|
||||
faraday-net_http (3.1.0)
|
||||
net-http
|
||||
faraday-net_http (3.4.0)
|
||||
net-http (>= 0.5.0)
|
||||
faraday-net_http_persistent (2.1.0)
|
||||
faraday (~> 2.5)
|
||||
net-http-persistent (~> 4.0)
|
||||
faraday-retry (2.2.1)
|
||||
faraday (~> 2.0)
|
||||
fast-mcp (1.5.0)
|
||||
addressable (~> 2.8)
|
||||
base64
|
||||
dry-schema (~> 1.14)
|
||||
json (~> 2.0)
|
||||
mime-types (~> 3.4)
|
||||
rack (~> 3.1)
|
||||
fcm (1.0.8)
|
||||
faraday (>= 1.0.0, < 3.0)
|
||||
googleauth (~> 1)
|
||||
@@ -417,7 +459,7 @@ GEM
|
||||
judoscale-sidekiq (1.8.2)
|
||||
judoscale-ruby (= 1.8.2)
|
||||
sidekiq (>= 5.0)
|
||||
jwt (2.8.1)
|
||||
jwt (2.10.1)
|
||||
base64
|
||||
kaminari (1.2.2)
|
||||
activesupport (>= 4.1.0)
|
||||
@@ -444,6 +486,16 @@ GEM
|
||||
logger (~> 1.6)
|
||||
letter_opener (1.10.0)
|
||||
launchy (>= 2.2, < 4)
|
||||
libdatadog (18.1.0.1.0)
|
||||
libdatadog (18.1.0.1.0-x86_64-linux)
|
||||
libddwaf (1.24.1.0.3)
|
||||
ffi (~> 1.0)
|
||||
libddwaf (1.24.1.0.3-arm64-darwin)
|
||||
ffi (~> 1.0)
|
||||
libddwaf (1.24.1.0.3-x86_64-darwin)
|
||||
ffi (~> 1.0)
|
||||
libddwaf (1.24.1.0.3-x86_64-linux)
|
||||
ffi (~> 1.0)
|
||||
line-bot-api (1.28.0)
|
||||
lint_roller (1.1.0)
|
||||
liquid (5.4.0)
|
||||
@@ -489,7 +541,7 @@ GEM
|
||||
mutex_m (0.3.0)
|
||||
neighbor (0.2.3)
|
||||
activerecord (>= 5.2)
|
||||
net-http (0.4.1)
|
||||
net-http (0.6.0)
|
||||
uri
|
||||
net-http-persistent (4.0.2)
|
||||
connection_pool (~> 2.2)
|
||||
@@ -577,7 +629,7 @@ GEM
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (2.2.15)
|
||||
rack (3.2.0)
|
||||
rack-attack (6.7.0)
|
||||
rack (>= 1.0, < 4)
|
||||
rack-contrib (2.5.0)
|
||||
@@ -586,33 +638,34 @@ GEM
|
||||
rack (>= 2.0.0)
|
||||
rack-mini-profiler (3.2.0)
|
||||
rack (>= 1.2.0)
|
||||
rack-protection (3.2.0)
|
||||
rack-protection (4.1.1)
|
||||
base64 (>= 0.1.0)
|
||||
rack (~> 2.2, >= 2.2.4)
|
||||
logger (>= 1.6.0)
|
||||
rack (>= 3.0.0, < 4)
|
||||
rack-proxy (0.7.7)
|
||||
rack
|
||||
rack-session (1.0.2)
|
||||
rack (< 3)
|
||||
rack-session (2.1.1)
|
||||
base64 (>= 0.1.0)
|
||||
rack (>= 3.0.0)
|
||||
rack-test (2.1.0)
|
||||
rack (>= 1.3)
|
||||
rack-timeout (0.6.3)
|
||||
rackup (1.0.1)
|
||||
rack (< 3)
|
||||
webrick
|
||||
rails (7.1.5.1)
|
||||
actioncable (= 7.1.5.1)
|
||||
actionmailbox (= 7.1.5.1)
|
||||
actionmailer (= 7.1.5.1)
|
||||
actionpack (= 7.1.5.1)
|
||||
actiontext (= 7.1.5.1)
|
||||
actionview (= 7.1.5.1)
|
||||
activejob (= 7.1.5.1)
|
||||
activemodel (= 7.1.5.1)
|
||||
activerecord (= 7.1.5.1)
|
||||
activestorage (= 7.1.5.1)
|
||||
activesupport (= 7.1.5.1)
|
||||
rackup (2.2.1)
|
||||
rack (>= 3)
|
||||
rails (7.1.5.2)
|
||||
actioncable (= 7.1.5.2)
|
||||
actionmailbox (= 7.1.5.2)
|
||||
actionmailer (= 7.1.5.2)
|
||||
actionpack (= 7.1.5.2)
|
||||
actiontext (= 7.1.5.2)
|
||||
actionview (= 7.1.5.2)
|
||||
activejob (= 7.1.5.2)
|
||||
activemodel (= 7.1.5.2)
|
||||
activerecord (= 7.1.5.2)
|
||||
activestorage (= 7.1.5.2)
|
||||
activesupport (= 7.1.5.2)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 7.1.5.1)
|
||||
railties (= 7.1.5.2)
|
||||
rails-dom-testing (2.2.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
@@ -620,9 +673,9 @@ GEM
|
||||
rails-html-sanitizer (1.6.1)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||
railties (7.1.5.1)
|
||||
actionpack (= 7.1.5.1)
|
||||
activesupport (= 7.1.5.1)
|
||||
railties (7.1.5.2)
|
||||
actionpack (= 7.1.5.2)
|
||||
activesupport (= 7.1.5.2)
|
||||
irb
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
@@ -720,7 +773,7 @@ GEM
|
||||
ruby2ruby (2.5.0)
|
||||
ruby_parser (~> 3.1)
|
||||
sexp_processor (~> 4.6)
|
||||
ruby_llm (1.3.1)
|
||||
ruby_llm (1.5.1)
|
||||
base64
|
||||
event_stream_parser (~> 1)
|
||||
faraday (>= 1.10.0)
|
||||
@@ -729,6 +782,7 @@ GEM
|
||||
faraday-retry (>= 1)
|
||||
marcel (~> 1.0)
|
||||
zeitwerk (~> 2)
|
||||
ruby_llm-schema (0.1.0)
|
||||
ruby_parser (3.20.0)
|
||||
sexp_processor (~> 4.16)
|
||||
sass (3.7.4)
|
||||
@@ -828,13 +882,17 @@ GEM
|
||||
telephone_number (1.4.20)
|
||||
test-prof (1.2.1)
|
||||
thor (1.4.0)
|
||||
tidewave (0.2.0)
|
||||
fast-mcp (~> 1.5.0)
|
||||
rack (>= 2.0)
|
||||
rails (>= 7.1.0)
|
||||
tilt (2.3.0)
|
||||
time_diff (0.3.0)
|
||||
activesupport
|
||||
i18n
|
||||
timeout (0.4.3)
|
||||
trailblazer-option (0.1.2)
|
||||
twilio-ruby (5.77.0)
|
||||
twilio-ruby (7.6.0)
|
||||
faraday (>= 0.9, < 3.0)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
nokogiri (>= 1.6, < 2.0)
|
||||
@@ -881,7 +939,6 @@ GEM
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webrick (1.9.1)
|
||||
websocket-driver (0.7.7)
|
||||
base64
|
||||
websocket-extensions (>= 0.1.0)
|
||||
@@ -910,7 +967,7 @@ DEPENDENCIES
|
||||
administrate (>= 0.20.1)
|
||||
administrate-field-active_storage (>= 1.0.3)
|
||||
administrate-field-belongs_to_search (>= 0.9.0)
|
||||
ai-agents (>= 0.2.1)
|
||||
ai-agents (>= 0.4.3)
|
||||
annotate
|
||||
attr_extras
|
||||
audited (~> 5.4, >= 5.4.1)
|
||||
@@ -927,7 +984,7 @@ DEPENDENCIES
|
||||
commonmarker
|
||||
csv-safe
|
||||
database_cleaner
|
||||
ddtrace
|
||||
datadog (~> 2.0)
|
||||
debug (~> 1.8)
|
||||
devise (>= 4.9.4)
|
||||
devise-secure_password!
|
||||
@@ -1004,6 +1061,7 @@ DEPENDENCIES
|
||||
rubocop-rails
|
||||
rubocop-rspec
|
||||
ruby-openai
|
||||
ruby_llm-schema
|
||||
scout_apm
|
||||
scss_lint
|
||||
seed_dump
|
||||
@@ -1024,8 +1082,9 @@ DEPENDENCIES
|
||||
stripe
|
||||
telephone_number
|
||||
test-prof
|
||||
tidewave
|
||||
time_diff
|
||||
twilio-ruby (~> 5.66)
|
||||
twilio-ruby
|
||||
twitty (~> 0.1.5)
|
||||
tzinfo-data
|
||||
uglifier
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
3.4.2
|
||||
3.4.3
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
class Api::V1::Accounts::AssignmentPolicies::InboxesController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_assignment_policy
|
||||
before_action -> { check_authorization(AssignmentPolicy) }
|
||||
|
||||
def index
|
||||
@inboxes = @assignment_policy.inboxes
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_assignment_policy
|
||||
@assignment_policy = Current.account.assignment_policies.find(
|
||||
params[:assignment_policy_id]
|
||||
)
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:assignment_policy_id)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,36 @@
|
||||
class Api::V1::Accounts::AssignmentPoliciesController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_assignment_policy, only: [:show, :update, :destroy]
|
||||
before_action :check_authorization
|
||||
|
||||
def index
|
||||
@assignment_policies = Current.account.assignment_policies
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@assignment_policy = Current.account.assignment_policies.create!(assignment_policy_params)
|
||||
end
|
||||
|
||||
def update
|
||||
@assignment_policy.update!(assignment_policy_params)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@assignment_policy.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_assignment_policy
|
||||
@assignment_policy = Current.account.assignment_policies.find(params[:id])
|
||||
end
|
||||
|
||||
def assignment_policy_params
|
||||
params.require(:assignment_policy).permit(
|
||||
:name, :description, :assignment_order, :conversation_priority,
|
||||
:fair_distribution_limit, :fair_distribution_window, :enabled
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,46 @@
|
||||
class Api::V1::Accounts::Inboxes::AssignmentPoliciesController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_inbox
|
||||
before_action :fetch_assignment_policy, only: [:create]
|
||||
before_action -> { check_authorization(AssignmentPolicy) }
|
||||
before_action :validate_assignment_policy, only: [:show, :destroy]
|
||||
|
||||
def show
|
||||
@assignment_policy = @inbox.assignment_policy
|
||||
end
|
||||
|
||||
def create
|
||||
# There should be only one assignment policy for an inbox.
|
||||
# If there is a new request to add an assignment policy, we will
|
||||
# delete the old one and attach the new policy
|
||||
remove_inbox_assignment_policy
|
||||
@inbox_assignment_policy = @inbox.create_inbox_assignment_policy!(assignment_policy: @assignment_policy)
|
||||
@assignment_policy = @inbox.assignment_policy
|
||||
end
|
||||
|
||||
def destroy
|
||||
remove_inbox_assignment_policy
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def remove_inbox_assignment_policy
|
||||
@inbox.inbox_assignment_policy&.destroy
|
||||
end
|
||||
|
||||
def fetch_inbox
|
||||
@inbox = Current.account.inboxes.find(permitted_params[:inbox_id])
|
||||
end
|
||||
|
||||
def fetch_assignment_policy
|
||||
@assignment_policy = Current.account.assignment_policies.find(permitted_params[:assignment_policy_id])
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:assignment_policy_id, :inbox_id)
|
||||
end
|
||||
|
||||
def validate_assignment_policy
|
||||
return render_not_found_error(I18n.t('errors.assignment_policy.not_found')) unless @inbox.assignment_policy
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,4 @@
|
||||
class Api::V1::Accounts::Whatsapp::AuthorizationsController < Api::V1::Accounts::BaseController
|
||||
before_action :validate_feature_enabled!
|
||||
before_action :fetch_and_validate_inbox, if: -> { params[:inbox_id].present? }
|
||||
|
||||
# POST /api/v1/accounts/:account_id/whatsapp/authorization
|
||||
@@ -65,15 +64,6 @@ class Api::V1::Accounts::Whatsapp::AuthorizationsController < Api::V1::Accounts:
|
||||
}, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def validate_feature_enabled!
|
||||
return if Current.account.feature_whatsapp_embedded_signup?
|
||||
|
||||
render json: {
|
||||
success: false,
|
||||
error: 'WhatsApp embedded signup is not enabled for this account'
|
||||
}, status: :forbidden
|
||||
end
|
||||
|
||||
def validate_embedded_signup_params!
|
||||
missing_params = []
|
||||
missing_params << 'code' if params[:code].blank?
|
||||
|
||||
@@ -18,12 +18,25 @@ module ReportingEventHelper
|
||||
end
|
||||
|
||||
def last_non_human_activity(conversation)
|
||||
# check if a handoff event already exists
|
||||
handoff_event = ReportingEvent.where(conversation_id: conversation.id, name: 'conversation_bot_handoff').last
|
||||
# Try to get either a handoff or reopened event first
|
||||
# These will always take precedence over any other activity
|
||||
# Also, any of these events can happen at any time in the course of a conversation lifecycle.
|
||||
# So we pick the latest event
|
||||
event = ReportingEvent.where(
|
||||
conversation_id: conversation.id,
|
||||
name: %w[conversation_bot_handoff conversation_opened]
|
||||
).order(event_end_time: :desc).first
|
||||
|
||||
# if a handoff exists, last non human activity is when the handoff ended,
|
||||
# otherwise it's when the conversation was created
|
||||
handoff_event&.event_end_time || conversation.created_at
|
||||
return event.event_end_time if event&.event_end_time
|
||||
|
||||
# Fallback to bot resolved event
|
||||
# Because this will be closest to the most accurate activity instead of conversation.created_at
|
||||
bot_event = ReportingEvent.where(conversation_id: conversation.id, name: 'conversation_bot_resolved').last
|
||||
|
||||
return bot_event.event_end_time if bot_event&.event_end_time
|
||||
|
||||
# If no events found, return conversation creation time
|
||||
conversation.created_at
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
+15
-106
@@ -9,6 +9,7 @@ import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue';
|
||||
import TagMultiSelectComboBox from 'dashboard/components-next/combobox/TagMultiSelectComboBox.vue';
|
||||
import WhatsAppTemplateParser from 'dashboard/components-next/whatsapp/WhatsAppTemplateParser.vue';
|
||||
|
||||
const emit = defineEmits(['submit', 'cancel']);
|
||||
|
||||
@@ -18,7 +19,9 @@ const formState = {
|
||||
uiFlags: useMapGetter('campaigns/getUIFlags'),
|
||||
labels: useMapGetter('labels/getLabels'),
|
||||
inboxes: useMapGetter('inboxes/getWhatsAppInboxes'),
|
||||
getWhatsAppTemplates: useMapGetter('inboxes/getWhatsAppTemplates'),
|
||||
getFilteredWhatsAppTemplates: useMapGetter(
|
||||
'inboxes/getFilteredWhatsAppTemplates'
|
||||
),
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
@@ -30,7 +33,7 @@ const initialState = {
|
||||
};
|
||||
|
||||
const state = reactive({ ...initialState });
|
||||
const processedParams = ref({});
|
||||
const templateParserRef = ref(null);
|
||||
|
||||
const rules = {
|
||||
title: { required, minLength: minLength(1) },
|
||||
@@ -67,7 +70,7 @@ const inboxOptions = computed(() =>
|
||||
|
||||
const templateOptions = computed(() => {
|
||||
if (!state.inboxId) return [];
|
||||
const templates = formState.getWhatsAppTemplates.value(state.inboxId);
|
||||
const templates = formState.getFilteredWhatsAppTemplates.value(state.inboxId);
|
||||
return templates.map(template => {
|
||||
// Create a more user-friendly label from template name
|
||||
const friendlyName = template.name
|
||||
@@ -88,26 +91,6 @@ const selectedTemplate = computed(() => {
|
||||
?.template;
|
||||
});
|
||||
|
||||
const templateString = computed(() => {
|
||||
if (!selectedTemplate.value) return '';
|
||||
try {
|
||||
return (
|
||||
selectedTemplate.value.components?.find(
|
||||
component => component.type === 'BODY'
|
||||
)?.text || ''
|
||||
);
|
||||
} catch (error) {
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
const processedString = computed(() => {
|
||||
if (!templateString.value) return '';
|
||||
return templateString.value.replace(/{{([^}]+)}}/g, (match, variable) => {
|
||||
return processedParams.value[variable] || `{{${variable}}}`;
|
||||
});
|
||||
});
|
||||
|
||||
const getErrorMessage = (field, errorKey) => {
|
||||
const baseKey = 'CAMPAIGN.WHATSAPP.CREATE.FORM';
|
||||
return v$.value[field].$error ? t(`${baseKey}.${errorKey}.ERROR`) : '';
|
||||
@@ -122,8 +105,7 @@ const formErrors = computed(() => ({
|
||||
}));
|
||||
|
||||
const hasRequiredTemplateParams = computed(() => {
|
||||
const params = Object.values(processedParams.value);
|
||||
return params.length === 0 || params.every(param => param.trim() !== '');
|
||||
return templateParserRef.value?.v$?.$invalid === false || true;
|
||||
});
|
||||
|
||||
const isSubmitDisabled = computed(
|
||||
@@ -135,32 +117,18 @@ const formatToUTCString = localDateTime =>
|
||||
|
||||
const resetState = () => {
|
||||
Object.assign(state, initialState);
|
||||
processedParams.value = {};
|
||||
v$.value.$reset();
|
||||
};
|
||||
|
||||
const handleCancel = () => emit('cancel');
|
||||
|
||||
const generateVariables = () => {
|
||||
const matchedVariables = templateString.value.match(/{{([^}]+)}}/g);
|
||||
if (!matchedVariables) {
|
||||
processedParams.value = {};
|
||||
return;
|
||||
}
|
||||
|
||||
const finalVars = matchedVariables.map(match => match.replace(/{{|}}/g, ''));
|
||||
processedParams.value = finalVars.reduce((acc, variable) => {
|
||||
acc[variable] = processedParams.value[variable] || '';
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const prepareCampaignDetails = () => {
|
||||
// Find the selected template to get its content
|
||||
const currentTemplate = selectedTemplate.value;
|
||||
const parserData = templateParserRef.value;
|
||||
|
||||
// Extract template content - this should be the template message body
|
||||
const templateContent = templateString.value;
|
||||
const templateContent = parserData?.renderedTemplate || '';
|
||||
|
||||
// Prepare template_params object with the same structure as used in contacts
|
||||
const templateParams = {
|
||||
@@ -168,7 +136,7 @@ const prepareCampaignDetails = () => {
|
||||
namespace: currentTemplate?.namespace || '',
|
||||
category: currentTemplate?.category || 'UTILITY',
|
||||
language: currentTemplate?.language || 'en_US',
|
||||
processed_params: processedParams.value,
|
||||
processed_params: parserData?.processedParams || {},
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -198,15 +166,6 @@ watch(
|
||||
() => state.inboxId,
|
||||
() => {
|
||||
state.templateId = null;
|
||||
processedParams.value = {};
|
||||
}
|
||||
);
|
||||
|
||||
// Generate variables when template changes
|
||||
watch(
|
||||
() => state.templateId,
|
||||
() => {
|
||||
generateVariables();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
@@ -254,62 +213,12 @@ watch(
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Template Preview -->
|
||||
<div
|
||||
<!-- Template Parser -->
|
||||
<WhatsAppTemplateParser
|
||||
v-if="selectedTemplate"
|
||||
class="flex flex-col gap-4 p-4 rounded-lg bg-n-alpha-black2"
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-sm font-medium text-n-slate-12">
|
||||
{{ selectedTemplate.name }}
|
||||
</h3>
|
||||
<span class="text-xs text-n-slate-11">
|
||||
{{ t('CAMPAIGN.WHATSAPP.CREATE.FORM.TEMPLATE.LANGUAGE') }}:
|
||||
{{ selectedTemplate.language || 'en' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="rounded-md bg-n-alpha-black3">
|
||||
<div class="text-sm whitespace-pre-wrap text-n-slate-12">
|
||||
{{ processedString }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-n-slate-11">
|
||||
{{ t('CAMPAIGN.WHATSAPP.CREATE.FORM.TEMPLATE.CATEGORY') }}:
|
||||
{{ selectedTemplate.category || 'UTILITY' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Template Variables -->
|
||||
<div
|
||||
v-if="Object.keys(processedParams).length > 0"
|
||||
class="flex flex-col gap-3"
|
||||
>
|
||||
<label class="text-sm font-medium text-n-slate-12">
|
||||
{{ t('CAMPAIGN.WHATSAPP.CREATE.FORM.TEMPLATE.VARIABLES_LABEL') }}
|
||||
</label>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div
|
||||
v-for="(value, key) in processedParams"
|
||||
:key="key"
|
||||
class="flex gap-2 items-center"
|
||||
>
|
||||
<Input
|
||||
v-model="processedParams[key]"
|
||||
type="text"
|
||||
class="flex-1"
|
||||
:placeholder="
|
||||
t('CAMPAIGN.WHATSAPP.CREATE.FORM.TEMPLATE.VARIABLE_PLACEHOLDER', {
|
||||
variable: key,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
ref="templateParserRef"
|
||||
:template="selectedTemplate"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="audience" class="mb-0.5 text-sm font-medium text-n-slate-12">
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<script setup>
|
||||
import { computed, useSlots } from 'vue';
|
||||
import { computed, useSlots, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { vOnClickOutside } from '@vueuse/components';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Breadcrumb from 'dashboard/components-next/breadcrumb/Breadcrumb.vue';
|
||||
import ComposeConversation from 'dashboard/components-next/NewConversation/ComposeConversation.vue';
|
||||
import VoiceCallButton from 'dashboard/components-next/Contacts/VoiceCallButton.vue';
|
||||
|
||||
const props = defineProps({
|
||||
selectedContact: {
|
||||
@@ -24,6 +26,8 @@ const { t } = useI18n();
|
||||
const slots = useSlots();
|
||||
const route = useRoute();
|
||||
|
||||
const isContactSidebarOpen = ref(false);
|
||||
|
||||
const contactId = computed(() => route.params.contactId);
|
||||
|
||||
const selectedContactName = computed(() => {
|
||||
@@ -56,6 +60,15 @@ const handleBreadcrumbClick = () => {
|
||||
const toggleBlock = () => {
|
||||
emit('toggleBlock', isContactBlocked.value);
|
||||
};
|
||||
|
||||
const handleConversationSidebarToggle = () => {
|
||||
isContactSidebarOpen.value = !isContactSidebarOpen.value;
|
||||
};
|
||||
|
||||
const closeMobileSidebar = () => {
|
||||
if (!isContactSidebarOpen.value) return;
|
||||
isContactSidebarOpen.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -67,7 +80,9 @@ const toggleBlock = () => {
|
||||
>
|
||||
<header class="sticky top-0 z-10 px-6 3xl:px-0">
|
||||
<div class="w-full mx-auto max-w-[40.625rem]">
|
||||
<div class="flex items-center justify-between w-full h-20 gap-2">
|
||||
<div
|
||||
class="flex flex-col xs:flex-row items-start xs:items-center justify-between w-full py-7 gap-2"
|
||||
>
|
||||
<Breadcrumb
|
||||
:items="breadcrumbItems"
|
||||
@click="handleBreadcrumbClick"
|
||||
@@ -85,6 +100,11 @@ const toggleBlock = () => {
|
||||
:disabled="isUpdating"
|
||||
@click="toggleBlock"
|
||||
/>
|
||||
<VoiceCallButton
|
||||
:phone="selectedContact?.phoneNumber"
|
||||
:label="$t('CONTACT_PANEL.CALL')"
|
||||
size="sm"
|
||||
/>
|
||||
<ComposeConversation :contact-id="contactId">
|
||||
<template #trigger="{ toggle }">
|
||||
<Button
|
||||
@@ -105,11 +125,65 @@ const toggleBlock = () => {
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Desktop sidebar -->
|
||||
<div
|
||||
v-if="slots.sidebar"
|
||||
class="overflow-y-auto justify-end min-w-52 w-full py-6 max-w-md border-l border-n-weak bg-n-solid-2"
|
||||
class="hidden lg:block overflow-y-auto justify-end min-w-52 w-full py-6 max-w-md border-l border-n-weak bg-n-solid-2"
|
||||
>
|
||||
<slot name="sidebar" />
|
||||
</div>
|
||||
|
||||
<!-- Mobile sidebar container -->
|
||||
<div
|
||||
v-if="slots.sidebar"
|
||||
class="lg:hidden fixed top-0 ltr:right-0 rtl:left-0 h-full z-50 flex justify-end transition-all duration-200 ease-in-out"
|
||||
:class="isContactSidebarOpen ? 'w-full' : 'w-16'"
|
||||
>
|
||||
<!-- Toggle button -->
|
||||
<div
|
||||
v-on-click-outside="[
|
||||
closeMobileSidebar,
|
||||
{ ignore: ['#contact-sidebar-content'] },
|
||||
]"
|
||||
class="flex items-start p-1 w-fit h-fit relative order-1 xs:top-24 top-28 transition-all bg-n-solid-2 border border-n-weak duration-500 ease-in-out"
|
||||
:class="[
|
||||
isContactSidebarOpen
|
||||
? 'justify-end ltr:rounded-l-full rtl:rounded-r-full ltr:rounded-r-none rtl:rounded-l-none'
|
||||
: 'justify-center rounded-full ltr:mr-6 rtl:ml-6',
|
||||
]"
|
||||
>
|
||||
<Button
|
||||
ghost
|
||||
slate
|
||||
sm
|
||||
class="!rounded-full rtl:rotate-180"
|
||||
:class="{ 'bg-n-alpha-2': isContactSidebarOpen }"
|
||||
:icon="
|
||||
isContactSidebarOpen
|
||||
? 'i-lucide-panel-right-close'
|
||||
: 'i-lucide-panel-right-open'
|
||||
"
|
||||
data-contact-sidebar-toggle
|
||||
@click="handleConversationSidebarToggle"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Transition
|
||||
enter-active-class="transition-transform duration-200 ease-in-out"
|
||||
leave-active-class="transition-transform duration-200 ease-in-out"
|
||||
enter-from-class="ltr:translate-x-full rtl:-translate-x-full"
|
||||
enter-to-class="ltr:translate-x-0 rtl:-translate-x-0"
|
||||
leave-from-class="ltr:translate-x-0 rtl:-translate-x-0"
|
||||
leave-to-class="ltr:translate-x-full rtl:-translate-x-full"
|
||||
>
|
||||
<div
|
||||
v-if="isContactSidebarOpen"
|
||||
id="contact-sidebar-content"
|
||||
class="order-2 w-[85%] sm:w-[50%] bg-n-solid-2 ltr:border-l rtl:border-r border-n-weak overflow-y-auto py-6 shadow-lg"
|
||||
>
|
||||
<slot name="sidebar" />
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
+57
-54
@@ -34,13 +34,13 @@ const emit = defineEmits([
|
||||
<template>
|
||||
<header class="sticky top-0 z-10">
|
||||
<div
|
||||
class="flex items-center justify-between w-full h-20 px-6 gap-2 mx-auto max-w-[60rem]"
|
||||
class="flex items-start sm:items-center justify-between w-full py-6 px-6 gap-2 mx-auto max-w-[60rem]"
|
||||
>
|
||||
<span class="text-xl font-medium truncate text-n-slate-12">
|
||||
{{ headerTitle }}
|
||||
</span>
|
||||
<div class="flex items-center flex-shrink-0 gap-4">
|
||||
<div v-if="showSearch" class="flex items-center gap-2">
|
||||
<div class="flex items-center flex-col sm:flex-row flex-shrink-0 gap-4">
|
||||
<div v-if="showSearch" class="flex items-center gap-2 w-full">
|
||||
<Input
|
||||
:model-value="searchValue"
|
||||
type="search"
|
||||
@@ -48,6 +48,7 @@ const emit = defineEmits([
|
||||
:custom-input-class="[
|
||||
'h-8 [&:not(.focus)]:!border-transparent bg-n-alpha-2 dark:bg-n-solid-1 ltr:!pl-8 !py-1 rtl:!pr-8',
|
||||
]"
|
||||
class="w-full"
|
||||
@input="emit('search', $event.target.value)"
|
||||
>
|
||||
<template #prefix>
|
||||
@@ -58,64 +59,66 @@ const emit = defineEmits([
|
||||
</template>
|
||||
</Input>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div v-if="!isLabelView && !isActiveView" class="relative">
|
||||
<div class="flex items-center flex-shrink-0 gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<div v-if="!isLabelView && !isActiveView" class="relative">
|
||||
<Button
|
||||
id="toggleContactsFilterButton"
|
||||
:icon="
|
||||
isSegmentsView ? 'i-lucide-pen-line' : 'i-lucide-list-filter'
|
||||
"
|
||||
color="slate"
|
||||
size="sm"
|
||||
class="relative w-8"
|
||||
variant="ghost"
|
||||
@click="emit('filter')"
|
||||
>
|
||||
<div
|
||||
v-if="hasActiveFilters && !isSegmentsView"
|
||||
class="absolute top-0 right-0 w-2 h-2 rounded-full bg-n-brand"
|
||||
/>
|
||||
</Button>
|
||||
<slot name="filter" />
|
||||
</div>
|
||||
<Button
|
||||
id="toggleContactsFilterButton"
|
||||
:icon="
|
||||
isSegmentsView ? 'i-lucide-pen-line' : 'i-lucide-list-filter'
|
||||
v-if="
|
||||
hasActiveFilters &&
|
||||
!isSegmentsView &&
|
||||
!isLabelView &&
|
||||
!isActiveView
|
||||
"
|
||||
icon="i-lucide-save"
|
||||
color="slate"
|
||||
size="sm"
|
||||
class="relative w-8"
|
||||
variant="ghost"
|
||||
@click="emit('filter')"
|
||||
>
|
||||
<div
|
||||
v-if="hasActiveFilters && !isSegmentsView"
|
||||
class="absolute top-0 right-0 w-2 h-2 rounded-full bg-n-brand"
|
||||
/>
|
||||
</Button>
|
||||
<slot name="filter" />
|
||||
@click="emit('createSegment')"
|
||||
/>
|
||||
<Button
|
||||
v-if="isSegmentsView && !isLabelView && !isActiveView"
|
||||
icon="i-lucide-trash"
|
||||
color="slate"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
@click="emit('deleteSegment')"
|
||||
/>
|
||||
<ContactSortMenu
|
||||
:active-sort="activeSort"
|
||||
:active-ordering="activeOrdering"
|
||||
@update:sort="emit('update:sort', $event)"
|
||||
/>
|
||||
<ContactMoreActions
|
||||
@add="emit('add')"
|
||||
@import="emit('import')"
|
||||
@export="emit('export')"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
v-if="
|
||||
hasActiveFilters &&
|
||||
!isSegmentsView &&
|
||||
!isLabelView &&
|
||||
!isActiveView
|
||||
"
|
||||
icon="i-lucide-save"
|
||||
color="slate"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
@click="emit('createSegment')"
|
||||
/>
|
||||
<Button
|
||||
v-if="isSegmentsView && !isLabelView && !isActiveView"
|
||||
icon="i-lucide-trash"
|
||||
color="slate"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
@click="emit('deleteSegment')"
|
||||
/>
|
||||
<ContactSortMenu
|
||||
:active-sort="activeSort"
|
||||
:active-ordering="activeOrdering"
|
||||
@update:sort="emit('update:sort', $event)"
|
||||
/>
|
||||
<ContactMoreActions
|
||||
@add="emit('add')"
|
||||
@import="emit('import')"
|
||||
@export="emit('export')"
|
||||
/>
|
||||
<div class="w-px h-4 bg-n-strong" />
|
||||
<ComposeConversation>
|
||||
<template #trigger="{ toggle }">
|
||||
<Button :label="buttonLabel" size="sm" @click="toggle" />
|
||||
</template>
|
||||
</ComposeConversation>
|
||||
</div>
|
||||
<div class="w-px h-4 bg-n-strong" />
|
||||
<ComposeConversation>
|
||||
<template #trigger="{ toggle }">
|
||||
<Button :label="buttonLabel" size="sm" @click="toggle" />
|
||||
</template>
|
||||
</ComposeConversation>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
+14
-11
@@ -291,17 +291,20 @@ defineExpose({
|
||||
@delete-segment="openDeleteSegmentDialog"
|
||||
>
|
||||
<template #filter>
|
||||
<ContactsFilter
|
||||
v-if="showFiltersModal"
|
||||
v-model="appliedFilter"
|
||||
:segment-name="activeSegmentName"
|
||||
:is-segment-view="hasActiveSegments"
|
||||
class="absolute mt-1 ltr:right-0 rtl:left-0 top-full"
|
||||
@apply-filter="onApplyFilter"
|
||||
@update-segment="onUpdateSegment"
|
||||
@close="closeAdvanceFiltersModal"
|
||||
@clear-filters="clearFilters"
|
||||
/>
|
||||
<div
|
||||
class="absolute mt-1 ltr:-right-52 rtl:-left-52 sm:ltr:right-0 sm:rtl:left-0 top-full"
|
||||
>
|
||||
<ContactsFilter
|
||||
v-if="showFiltersModal"
|
||||
v-model="appliedFilter"
|
||||
:segment-name="activeSegmentName"
|
||||
:is-segment-view="hasActiveSegments"
|
||||
@apply-filter="onApplyFilter"
|
||||
@update-segment="onUpdateSegment"
|
||||
@close="closeAdvanceFiltersModal"
|
||||
@clear-filters="clearFilters"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ContactsHeader>
|
||||
|
||||
|
||||
+1
-1
@@ -105,7 +105,7 @@ const handleOrderChange = value => {
|
||||
<div
|
||||
v-if="isMenuOpen"
|
||||
v-on-clickaway="() => (isMenuOpen = false)"
|
||||
class="absolute top-full mt-1 ltr:right-0 rtl:left-0 flex flex-col gap-4 bg-n-alpha-3 backdrop-blur-[100px] border border-n-weak w-72 rounded-xl p-4"
|
||||
class="absolute top-full mt-1 ltr:-right-32 rtl:-left-32 sm:ltr:right-0 sm:rtl:left-0 flex flex-col gap-4 bg-n-alpha-3 backdrop-blur-[100px] border border-n-weak w-72 rounded-xl p-4"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-sm text-n-slate-12">
|
||||
|
||||
@@ -96,10 +96,7 @@ const openFilter = () => {
|
||||
<slot name="default" />
|
||||
</div>
|
||||
</main>
|
||||
<footer
|
||||
v-if="showPaginationFooter"
|
||||
class="sticky bottom-0 z-10 px-4 pb-4"
|
||||
>
|
||||
<footer v-if="showPaginationFooter" class="sticky bottom-0 z-0 px-4 pb-4">
|
||||
<PaginationFooter
|
||||
current-page-info="CONTACTS_LAYOUT.PAGINATION_FOOTER.SHOWING"
|
||||
:current-page="currentPage"
|
||||
|
||||
+3
-3
@@ -4,8 +4,8 @@ export default [
|
||||
city: 'Los Angeles',
|
||||
country: 'United States',
|
||||
description:
|
||||
"I'm Candice, a developer focusing on building web solutions. Currently, I’m working as a Product Developer at Chatwoot.",
|
||||
companyName: 'Chatwoot',
|
||||
"I'm Candice, a developer focusing on building web solutions. Currently, I’m working as a Product Developer at Lumora.",
|
||||
companyName: 'Lumora',
|
||||
countryCode: 'US',
|
||||
socialProfiles: {
|
||||
github: 'candice-dev',
|
||||
@@ -16,7 +16,7 @@ export default [
|
||||
},
|
||||
},
|
||||
availabilityStatus: 'offline',
|
||||
email: 'candice.matherson@chatwoot.com',
|
||||
email: 'candice.matherson@lumora.com',
|
||||
id: 22,
|
||||
name: 'Candice Matherson',
|
||||
phoneNumber: '+14155552671',
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<script setup>
|
||||
import { computed, ref, useAttrs } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { INBOX_TYPES } from 'dashboard/helper/inbox';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
|
||||
const props = defineProps({
|
||||
phone: { type: String, default: '' },
|
||||
label: { type: String, default: '' },
|
||||
icon: { type: [String, Object, Function], default: '' },
|
||||
size: { type: String, default: 'sm' },
|
||||
tooltipLabel: { type: String, default: '' },
|
||||
});
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
const attrs = useAttrs();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const inboxesList = useMapGetter('inboxes/getInboxes');
|
||||
const voiceInboxes = computed(() =>
|
||||
(inboxesList.value || []).filter(
|
||||
inbox => inbox.channel_type === INBOX_TYPES.VOICE
|
||||
)
|
||||
);
|
||||
const hasVoiceInboxes = computed(() => voiceInboxes.value.length > 0);
|
||||
|
||||
// Unified behavior: hide when no phone
|
||||
const shouldRender = computed(() => hasVoiceInboxes.value && !!props.phone);
|
||||
|
||||
const dialogRef = ref(null);
|
||||
|
||||
const onClick = () => {
|
||||
if (voiceInboxes.value.length > 1) {
|
||||
dialogRef.value?.open();
|
||||
return;
|
||||
}
|
||||
useAlert(t('CONTACT_PANEL.CALL_UNDER_DEVELOPMENT'));
|
||||
};
|
||||
|
||||
const onPickInbox = () => {
|
||||
// Placeholder until actual call wiring happens
|
||||
useAlert(t('CONTACT_PANEL.CALL_UNDER_DEVELOPMENT'));
|
||||
dialogRef.value?.close();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="contents">
|
||||
<Button
|
||||
v-if="shouldRender"
|
||||
v-tooltip.top-end="tooltipLabel || null"
|
||||
v-bind="attrs"
|
||||
:label="label"
|
||||
:icon="icon"
|
||||
:size="size"
|
||||
@click="onClick"
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
v-if="shouldRender && voiceInboxes.length > 1"
|
||||
ref="dialogRef"
|
||||
:title="$t('CONTACT_PANEL.VOICE_INBOX_PICKER.TITLE')"
|
||||
show-cancel-button
|
||||
:show-confirm-button="false"
|
||||
width="md"
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<button
|
||||
v-for="inbox in voiceInboxes"
|
||||
:key="inbox.id"
|
||||
type="button"
|
||||
class="flex items-center justify-between w-full px-4 py-2 text-left rounded-lg hover:bg-n-alpha-2"
|
||||
@click="onPickInbox(inbox)"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="i-ri-phone-fill text-n-slate-10" />
|
||||
<span class="text-sm text-n-slate-12">{{ inbox.name }}</span>
|
||||
</div>
|
||||
<span v-if="inbox.phone_number" class="text-xs text-n-slate-10">
|
||||
{{ inbox.phone_number }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</Dialog>
|
||||
</span>
|
||||
</template>
|
||||
+1
@@ -47,6 +47,7 @@ const unreadMessagesCount = computed(() => {
|
||||
</p>
|
||||
<div class="flex items-center flex-shrink-0 gap-2 pb-2">
|
||||
<Avatar
|
||||
v-if="assignee.name"
|
||||
:name="assignee.name"
|
||||
:src="assignee.thumbnail"
|
||||
:size="20"
|
||||
|
||||
+1
@@ -96,6 +96,7 @@ defineExpose({
|
||||
/>
|
||||
</div>
|
||||
<Avatar
|
||||
v-if="assignee.name"
|
||||
:name="assignee.name"
|
||||
:src="assignee.thumbnail"
|
||||
:size="20"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useWindowSize } from '@vueuse/core';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { vOnClickOutside } from '@vueuse/components';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
processContactableInboxes,
|
||||
mergeInboxDetails,
|
||||
} from 'dashboard/components-next/NewConversation/helpers/composeConversationHelper';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
|
||||
import ComposeNewConversationForm from 'dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue';
|
||||
|
||||
@@ -37,9 +39,16 @@ const emit = defineEmits(['close']);
|
||||
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
const { width: windowWidth } = useWindowSize();
|
||||
|
||||
const { fetchSignatureFlagFromUISettings } = useUISettings();
|
||||
|
||||
const isSmallScreen = computed(
|
||||
() => windowWidth.value < wootConstants.SMALL_SCREEN_BREAKPOINT
|
||||
);
|
||||
|
||||
const viewInModal = computed(() => props.isModal || isSmallScreen.value);
|
||||
|
||||
const contacts = ref([]);
|
||||
const selectedContact = ref(null);
|
||||
const targetInbox = ref(null);
|
||||
@@ -67,7 +76,7 @@ const directUploadsEnabled = computed(
|
||||
const activeContact = computed(() => contactById.value(props.contactId));
|
||||
|
||||
const composePopoverClass = computed(() => {
|
||||
if (props.isModal) return '';
|
||||
if (viewInModal.value) return '';
|
||||
|
||||
return props.alignPosition === 'right'
|
||||
? 'absolute ltr:left-0 ltr:right-[unset] rtl:right-0 rtl:left-[unset]'
|
||||
@@ -179,14 +188,18 @@ const toggle = () => {
|
||||
|
||||
watch(
|
||||
activeContact,
|
||||
() => {
|
||||
if (activeContact.value && props.contactId) {
|
||||
const contactInboxes = activeContact.value?.contactInboxes || [];
|
||||
(currentContact, previousContact) => {
|
||||
if (currentContact && props.contactId) {
|
||||
// Reset on contact change
|
||||
if (currentContact?.id !== previousContact?.id) clearSelectedContact();
|
||||
|
||||
// First process the contactable inboxes to get the right structure
|
||||
const processedInboxes = processContactableInboxes(contactInboxes);
|
||||
const processedInboxes = processContactableInboxes(
|
||||
currentContact.contactInboxes || []
|
||||
);
|
||||
// Then Merge processedInboxes with the inboxes list
|
||||
selectedContact.value = {
|
||||
...activeContact.value,
|
||||
...currentContact,
|
||||
contactInboxes: mergeInboxDetails(processedInboxes, inboxesList.value),
|
||||
};
|
||||
}
|
||||
@@ -202,7 +215,7 @@ const handleClickOutside = () => {
|
||||
};
|
||||
|
||||
const onModalBackdropClick = () => {
|
||||
if (!props.isModal) return;
|
||||
if (!viewInModal.value) return;
|
||||
handleClickOutside();
|
||||
};
|
||||
|
||||
@@ -231,7 +244,7 @@ useKeyboardEvents(keyboardEvents);
|
||||
]"
|
||||
class="relative"
|
||||
:class="{
|
||||
'z-40': showComposeNewConversation,
|
||||
'z-50': showComposeNewConversation && !viewInModal,
|
||||
}"
|
||||
>
|
||||
<slot
|
||||
@@ -243,12 +256,12 @@ useKeyboardEvents(keyboardEvents);
|
||||
v-if="showComposeNewConversation"
|
||||
:class="{
|
||||
'fixed z-50 bg-n-alpha-black1 backdrop-blur-[4px] flex items-start pt-[clamp(3rem,15vh,12rem)] justify-center inset-0':
|
||||
isModal,
|
||||
viewInModal,
|
||||
}"
|
||||
@click.self="onModalBackdropClick"
|
||||
>
|
||||
<ComposeNewConversationForm
|
||||
:class="[{ 'mt-2': !isModal }, composePopoverClass]"
|
||||
:class="[{ 'mt-2': !viewInModal }, composePopoverClass]"
|
||||
:contacts="contacts"
|
||||
:contact-id="contactId"
|
||||
:is-loading="isSearching"
|
||||
|
||||
+4
-2
@@ -25,6 +25,7 @@ const props = defineProps({
|
||||
hasNoInbox: { type: Boolean, default: false },
|
||||
isDropdownActive: { type: Boolean, default: false },
|
||||
messageSignature: { type: String, default: '' },
|
||||
inboxId: { type: Number, default: null },
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
@@ -150,9 +151,10 @@ useKeyboardEvents(keyboardEvents);
|
||||
<div
|
||||
class="flex items-center justify-between w-full h-[3.25rem] gap-2 px-4 py-3"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex gap-2 items-center">
|
||||
<WhatsAppOptions
|
||||
v-if="isWhatsappInbox"
|
||||
:inbox-id="inboxId"
|
||||
:message-templates="messageTemplates"
|
||||
@send-message="emit('sendWhatsappMessage', $event)"
|
||||
/>
|
||||
@@ -206,7 +208,7 @@ useKeyboardEvents(keyboardEvents);
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex gap-2 items-center">
|
||||
<Button
|
||||
:label="t('COMPOSE_NEW_CONVERSATION.FORM.ACTION_BUTTONS.DISCARD')"
|
||||
variant="faded"
|
||||
|
||||
+2
-1
@@ -265,7 +265,7 @@ const handleSendWhatsappMessage = async ({ message, templateParams }) => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="w-[42rem] divide-y divide-n-strong overflow-visible transition-all duration-300 ease-in-out top-full justify-between flex flex-col bg-n-alpha-3 border border-n-strong shadow-sm backdrop-blur-[100px] rounded-xl"
|
||||
class="w-[42rem] divide-y divide-n-strong overflow-visible transition-all duration-300 ease-in-out top-full justify-between flex flex-col bg-n-alpha-3 border border-n-strong shadow-sm backdrop-blur-[100px] rounded-xl min-w-0"
|
||||
>
|
||||
<ContactSelector
|
||||
:contacts="contacts"
|
||||
@@ -336,6 +336,7 @@ const handleSendWhatsappMessage = async ({ message, templateParams }) => {
|
||||
:is-loading="isCreating"
|
||||
:disable-send-button="isCreating"
|
||||
:has-selected-inbox="!!targetInbox"
|
||||
:inbox-id="targetInbox?.id"
|
||||
:has-no-inbox="showNoInboxAlert"
|
||||
:is-dropdown-active="isAnyDropdownActive"
|
||||
:message-signature="messageSignature"
|
||||
|
||||
+12
-23
@@ -1,24 +1,25 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import WhatsappTemplateParser from './WhatsappTemplateParser.vue';
|
||||
import WhatsappTemplate from './WhatsappTemplate.vue';
|
||||
|
||||
const props = defineProps({
|
||||
messageTemplates: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
inboxId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['sendMessage']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// TODO: Remove this when we support all formats
|
||||
const formatsToRemove = ['DOCUMENT', 'IMAGE', 'VIDEO'];
|
||||
const getFilteredWhatsAppTemplates = useMapGetter(
|
||||
'inboxes/getFilteredWhatsAppTemplates'
|
||||
);
|
||||
|
||||
const searchQuery = ref('');
|
||||
const selectedTemplate = ref(null);
|
||||
@@ -26,19 +27,7 @@ const selectedTemplate = ref(null);
|
||||
const showTemplatesMenu = ref(false);
|
||||
|
||||
const whatsAppTemplateMessages = computed(() => {
|
||||
// Add null check and ensure it's an array
|
||||
const templates = Array.isArray(props.messageTemplates)
|
||||
? props.messageTemplates
|
||||
: [];
|
||||
|
||||
// TODO: Remove the last filter when we support all formats
|
||||
return templates
|
||||
.filter(template => template?.status?.toLowerCase() === 'approved')
|
||||
.filter(template => {
|
||||
return template?.components?.every(component => {
|
||||
return !formatsToRemove.includes(component.format);
|
||||
});
|
||||
});
|
||||
return getFilteredWhatsAppTemplates.value(props.inboxId);
|
||||
});
|
||||
|
||||
const filteredTemplates = computed(() => {
|
||||
@@ -106,7 +95,7 @@ const handleSendMessage = template => {
|
||||
<div
|
||||
v-for="template in filteredTemplates"
|
||||
:key="template.id"
|
||||
class="flex flex-col w-full gap-2 p-2 rounded-lg cursor-pointer dark:hover:bg-n-alpha-3 hover:bg-n-alpha-1"
|
||||
class="flex flex-col gap-2 p-2 w-full rounded-lg cursor-pointer dark:hover:bg-n-alpha-3 hover:bg-n-alpha-1"
|
||||
@click="handleTemplateClick(template)"
|
||||
>
|
||||
<span class="text-sm text-n-slate-12">{{ template.name }}</span>
|
||||
@@ -115,12 +104,12 @@ const handleSendMessage = template => {
|
||||
</p>
|
||||
</div>
|
||||
<template v-if="filteredTemplates.length === 0">
|
||||
<p class="w-full pt-2 text-sm text-n-slate-11">
|
||||
<p class="pt-2 w-full text-sm text-n-slate-11">
|
||||
{{ t('COMPOSE_NEW_CONVERSATION.FORM.WHATSAPP_OPTIONS.EMPTY_STATE') }}
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
<WhatsappTemplateParser
|
||||
<WhatsappTemplate
|
||||
v-if="selectedTemplate"
|
||||
:template="selectedTemplate"
|
||||
@send-message="handleSendMessage"
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
<script setup>
|
||||
import WhatsAppTemplateParser from 'dashboard/components-next/whatsapp/WhatsAppTemplateParser.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
defineProps({
|
||||
template: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['sendMessage', 'back']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const handleSendMessage = payload => {
|
||||
emit('sendMessage', payload);
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
emit('back');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="absolute top-full mt-1.5 max-h-[30rem] overflow-y-auto ltr:left-0 rtl:right-0 flex flex-col gap-4 px-4 pt-6 pb-5 items-start w-[28.75rem] h-auto bg-n-solid-2 border border-n-strong shadow-sm rounded-lg"
|
||||
>
|
||||
<div class="w-full">
|
||||
<WhatsAppTemplateParser
|
||||
:template="template"
|
||||
@send-message="handleSendMessage"
|
||||
@back="handleBack"
|
||||
>
|
||||
<template #actions="{ sendMessage, goBack, disabled }">
|
||||
<div class="flex gap-3 justify-between items-end w-full h-14">
|
||||
<Button
|
||||
:label="
|
||||
t(
|
||||
'COMPOSE_NEW_CONVERSATION.FORM.WHATSAPP_OPTIONS.TEMPLATE_PARSER.BACK'
|
||||
)
|
||||
"
|
||||
color="slate"
|
||||
variant="faded"
|
||||
class="w-full font-medium"
|
||||
@click="goBack"
|
||||
/>
|
||||
<Button
|
||||
:label="
|
||||
t(
|
||||
'COMPOSE_NEW_CONVERSATION.FORM.WHATSAPP_OPTIONS.TEMPLATE_PARSER.SEND_MESSAGE'
|
||||
)
|
||||
"
|
||||
class="w-full font-medium"
|
||||
:disabled="disabled"
|
||||
@click="sendMessage"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</WhatsAppTemplateParser>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -21,9 +21,17 @@ const onClick = (item, index) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav :aria-label="t('BREADCRUMB.ARIA_LABEL')" class="flex items-center h-8">
|
||||
<ol class="flex items-center mb-0">
|
||||
<li v-for="(item, index) in items" :key="index" class="flex items-center">
|
||||
<nav
|
||||
:aria-label="t('BREADCRUMB.ARIA_LABEL')"
|
||||
class="flex items-center h-8 min-w-0"
|
||||
>
|
||||
<ol class="flex items-center mb-0 min-w-0">
|
||||
<li
|
||||
v-for="(item, index) in items"
|
||||
:key="index"
|
||||
class="flex items-center"
|
||||
:class="{ 'min-w-0 flex-1': index === items.length - 1 }"
|
||||
>
|
||||
<Icon
|
||||
v-if="index > 0"
|
||||
icon="i-lucide-chevron-right"
|
||||
@@ -40,7 +48,7 @@ const onClick = (item, index) => {
|
||||
</button>
|
||||
|
||||
<!-- The last breadcrumb item is plain text -->
|
||||
<span v-else class="text-sm truncate text-n-slate-12 max-w-56">
|
||||
<span v-else class="text-sm truncate text-n-slate-12 min-w-0 block">
|
||||
{{ item.emoji ? item.emoji : '' }} {{ item.label }}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
@@ -52,10 +52,10 @@ const handleBreadcrumbClick = item => {
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="mt-4 px-10 flex flex-col w-full h-screen overflow-y-auto bg-n-background"
|
||||
class="px-6 flex flex-col w-full h-screen overflow-y-auto bg-n-background"
|
||||
>
|
||||
<div class="max-w-[60rem] mx-auto flex flex-col w-full h-full mb-4">
|
||||
<header class="mb-7 sticky top-0 z-10 bg-n-background">
|
||||
<header class="mb-7 sticky top-0 bg-n-background pt-4 z-20">
|
||||
<Breadcrumb :items="breadcrumbItems" @click="handleBreadcrumbClick" />
|
||||
</header>
|
||||
<main class="flex gap-16 w-full flex-1 pb-16">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import { computed, h, reactive } from 'vue';
|
||||
import { computed, h, reactive, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useToggle } from '@vueuse/core';
|
||||
import { useToggle, useElementSize } from '@vueuse/core';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, minLength } from '@vuelidate/validators';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
@@ -11,6 +11,7 @@ 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';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
@@ -31,7 +32,7 @@ const props = defineProps({
|
||||
},
|
||||
tools: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => [],
|
||||
},
|
||||
selectable: {
|
||||
type: Boolean,
|
||||
@@ -60,7 +61,13 @@ const state = reactive({
|
||||
instruction: '',
|
||||
});
|
||||
|
||||
const instructionContentRef = ref();
|
||||
|
||||
const [isEditing, toggleEditing] = useToggle();
|
||||
const [isInstructionExpanded, toggleInstructionExpanded] = useToggle();
|
||||
|
||||
const { height: contentHeight } = useElementSize(instructionContentRef);
|
||||
const needsOverlay = computed(() => contentHeight.value > 160);
|
||||
|
||||
const startEdit = () => {
|
||||
Object.assign(state, {
|
||||
@@ -111,7 +118,7 @@ const LINK_INSTRUCTION_CLASS =
|
||||
|
||||
const renderInstruction = instruction => () =>
|
||||
h('p', {
|
||||
class: `text-sm text-n-slate-12 py-4 mb-0 [&_ol]:list-decimal ${LINK_INSTRUCTION_CLASS}`,
|
||||
class: `text-sm text-n-slate-12 py-4 mb-0 prose prose-sm min-w-0 break-words max-w-none ${LINK_INSTRUCTION_CLASS}`,
|
||||
innerHTML: instruction,
|
||||
});
|
||||
</script>
|
||||
@@ -157,8 +164,38 @@ const renderInstruction = instruction => () =>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<component :is="renderInstruction(formatMessage(instruction, false))" />
|
||||
<span class="text-sm text-n-slate-11 font-medium mb-1">
|
||||
|
||||
<div
|
||||
class="relative overflow-hidden transition-all duration-300 ease-in-out group/expandable"
|
||||
:class="{ 'cursor-pointer': needsOverlay }"
|
||||
:style="{
|
||||
maxHeight: isInstructionExpanded ? `${contentHeight}px` : '10rem',
|
||||
}"
|
||||
@click="needsOverlay ? toggleInstructionExpanded() : null"
|
||||
>
|
||||
<div ref="instructionContentRef">
|
||||
<component
|
||||
:is="renderInstruction(formatMessage(instruction, false))"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="absolute bottom-0 w-full flex items-end justify-center text-xs text-n-slate-11 bg-gradient-to-t h-40 from-n-solid-2 via-n-solid-2 via-10% to-transparent transition-all duration-500 ease-in-out px-2 py-1 rounded pointer-events-none"
|
||||
:class="{
|
||||
'visible opacity-100': !isInstructionExpanded,
|
||||
'invisible opacity-0': isInstructionExpanded || !needsOverlay,
|
||||
}"
|
||||
>
|
||||
<Icon
|
||||
icon="i-lucide-chevron-down"
|
||||
class="text-n-slate-7 mb-4 size-4 group-hover/expandable:text-n-slate-11 transition-colors duration-200"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
v-if="tools?.length"
|
||||
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>
|
||||
|
||||
@@ -192,6 +192,7 @@ defineExpose({ validate });
|
||||
solid
|
||||
slate
|
||||
icon="i-lucide-trash"
|
||||
class="flex-shrink-0"
|
||||
@click.stop="emit('remove')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -104,7 +104,7 @@ const outsideClickHandler = [
|
||||
<template>
|
||||
<div
|
||||
v-on-click-outside="outsideClickHandler"
|
||||
class="z-40 max-w-3xl lg:w-[750px] overflow-visible w-full border border-n-weak bg-n-alpha-3 backdrop-blur-[100px] shadow-lg rounded-xl p-6 grid gap-6"
|
||||
class="z-40 max-w-3xl min-w-96 lg:w-[750px] overflow-visible w-full border border-n-weak bg-n-alpha-3 backdrop-blur-[100px] shadow-lg rounded-xl p-6 grid gap-6"
|
||||
>
|
||||
<h3 class="text-base font-medium leading-6 text-n-slate-12">
|
||||
{{ filterModalHeaderTitle }}
|
||||
@@ -146,10 +146,10 @@ const outsideClickHandler = [
|
||||
</template>
|
||||
</ul>
|
||||
<div class="flex justify-between gap-2">
|
||||
<Button sm ghost blue @click="addFilter">
|
||||
<Button sm ghost blue class="flex-shrink-0" @click="addFilter">
|
||||
{{ $t('CONTACTS_LAYOUT.FILTER.BUTTONS.ADD_FILTER') }}
|
||||
</Button>
|
||||
<div class="flex gap-2">
|
||||
<div class="flex gap-2 flex-shrink-0">
|
||||
<Button sm faded slate @click="resetFilter">
|
||||
{{ $t('CONTACTS_LAYOUT.FILTER.BUTTONS.CLEAR_FILTERS') }}
|
||||
</Button>
|
||||
|
||||
@@ -509,10 +509,8 @@ const menuItems = computed(() => {
|
||||
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,
|
||||
'shadow-lg md:shadow-none': isMobileSidebarOpen,
|
||||
'ltr:-translate-x-full rtl:translate-x-full': !isMobileSidebarOpen,
|
||||
},
|
||||
]"
|
||||
>
|
||||
|
||||
@@ -86,6 +86,15 @@ const menuItems = computed(() => {
|
||||
nativeLink: true,
|
||||
target: '_blank',
|
||||
},
|
||||
{
|
||||
show: true,
|
||||
showOnCustomBrandedInstance: false,
|
||||
label: t('SIDEBAR_ITEMS.CHANGELOG'),
|
||||
icon: 'i-lucide-scroll-text',
|
||||
link: 'https://www.chatwoot.com/changelog/',
|
||||
nativeLink: true,
|
||||
target: '_blank',
|
||||
},
|
||||
{
|
||||
show: currentUser.value.type === 'SuperAdmin',
|
||||
showOnCustomBrandedInstance: true,
|
||||
@@ -114,7 +123,7 @@ const allowedMenuItems = computed(() => {
|
||||
<DropdownContainer class="relative w-full min-w-0" @close="emit('close')">
|
||||
<template #trigger="{ toggle, isOpen }">
|
||||
<button
|
||||
class="flex gap-2 items-center rounded-lg cursor-pointer text-left w-full hover:bg-n-alpha-1 p-1"
|
||||
class="flex gap-2 items-center p-1 w-full text-left rounded-lg cursor-pointer hover:bg-n-alpha-1"
|
||||
:class="{ 'bg-n-alpha-1': isOpen }"
|
||||
@click="toggle"
|
||||
>
|
||||
@@ -127,16 +136,16 @@ const allowedMenuItems = computed(() => {
|
||||
rounded-full
|
||||
/>
|
||||
<div class="min-w-0">
|
||||
<div class="text-n-slate-12 text-sm leading-4 font-medium truncate">
|
||||
<div class="text-sm font-medium leading-4 truncate text-n-slate-12">
|
||||
{{ currentUser.available_name }}
|
||||
</div>
|
||||
<div class="text-n-slate-11 text-xs truncate">
|
||||
<div class="text-xs truncate text-n-slate-11">
|
||||
{{ currentUser.email }}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
<DropdownBody class="ltr:left-0 rtl:right-0 bottom-12 z-50 w-80 mb-2">
|
||||
<DropdownBody class="bottom-12 z-50 mb-2 w-80 ltr:left-0 rtl:right-0">
|
||||
<SidebarProfileMenuStatus />
|
||||
<DropdownSeparator />
|
||||
<template v-for="item in allowedMenuItems" :key="item.label">
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { requiredIf } from '@vuelidate/validators';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import {
|
||||
buildTemplateParameters,
|
||||
allKeysRequired,
|
||||
replaceTemplateVariables,
|
||||
DEFAULT_LANGUAGE,
|
||||
DEFAULT_CATEGORY,
|
||||
COMPONENT_TYPES,
|
||||
MEDIA_FORMATS,
|
||||
findComponentByType,
|
||||
} from 'dashboard/helper/templateHelper';
|
||||
|
||||
const props = defineProps({
|
||||
template: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
validator: value => {
|
||||
if (!value || typeof value !== 'object') return false;
|
||||
if (!value.components || !Array.isArray(value.components)) return false;
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['sendMessage', 'resetTemplate', 'back']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const processedParams = ref({});
|
||||
|
||||
const languageLabel = computed(() => {
|
||||
return `${t('WHATSAPP_TEMPLATES.PARSER.LANGUAGE')}: ${props.template.language || DEFAULT_LANGUAGE}`;
|
||||
});
|
||||
|
||||
const categoryLabel = computed(() => {
|
||||
return `${t('WHATSAPP_TEMPLATES.PARSER.CATEGORY')}: ${props.template.category || DEFAULT_CATEGORY}`;
|
||||
});
|
||||
|
||||
const headerComponent = computed(() => {
|
||||
return findComponentByType(props.template, COMPONENT_TYPES.HEADER);
|
||||
});
|
||||
|
||||
const bodyComponent = computed(() => {
|
||||
return findComponentByType(props.template, COMPONENT_TYPES.BODY);
|
||||
});
|
||||
|
||||
const bodyText = computed(() => {
|
||||
return bodyComponent.value?.text || '';
|
||||
});
|
||||
|
||||
const hasMediaHeader = computed(() =>
|
||||
MEDIA_FORMATS.includes(headerComponent.value?.format)
|
||||
);
|
||||
|
||||
const formatType = computed(() => {
|
||||
const format = headerComponent.value?.format;
|
||||
return format ? format.charAt(0) + format.slice(1).toLowerCase() : '';
|
||||
});
|
||||
|
||||
const hasVariables = computed(() => {
|
||||
return bodyText.value?.match(/{{([^}]+)}}/g) !== null;
|
||||
});
|
||||
|
||||
const renderedTemplate = computed(() => {
|
||||
return replaceTemplateVariables(bodyText.value, processedParams.value);
|
||||
});
|
||||
|
||||
const isFormInvalid = computed(() => {
|
||||
if (!hasVariables.value && !hasMediaHeader.value) return false;
|
||||
|
||||
if (hasMediaHeader.value && !processedParams.value.header?.media_url) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hasVariables.value && processedParams.value.body) {
|
||||
const hasEmptyBodyVariable = Object.values(processedParams.value.body).some(
|
||||
value => !value
|
||||
);
|
||||
if (hasEmptyBodyVariable) return true;
|
||||
}
|
||||
|
||||
if (processedParams.value.buttons) {
|
||||
const hasEmptyButtonParameter = processedParams.value.buttons.some(
|
||||
button => !button.parameter
|
||||
);
|
||||
if (hasEmptyButtonParameter) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
const v$ = useVuelidate(
|
||||
{
|
||||
processedParams: {
|
||||
requiredIfKeysPresent: requiredIf(hasVariables),
|
||||
allKeysRequired,
|
||||
},
|
||||
},
|
||||
{ processedParams }
|
||||
);
|
||||
|
||||
const initializeTemplateParameters = () => {
|
||||
processedParams.value = buildTemplateParameters(
|
||||
props.template,
|
||||
hasMediaHeader.value
|
||||
);
|
||||
};
|
||||
|
||||
const updateMediaUrl = value => {
|
||||
processedParams.value.header ??= {};
|
||||
processedParams.value.header.media_url = value;
|
||||
};
|
||||
|
||||
const sendMessage = () => {
|
||||
v$.value.$touch();
|
||||
if (v$.value.$invalid) return;
|
||||
|
||||
const { name, category, language, namespace } = props.template;
|
||||
|
||||
const payload = {
|
||||
message: renderedTemplate.value,
|
||||
templateParams: {
|
||||
name,
|
||||
category,
|
||||
language,
|
||||
namespace,
|
||||
processed_params: processedParams.value,
|
||||
},
|
||||
};
|
||||
emit('sendMessage', payload);
|
||||
};
|
||||
|
||||
const resetTemplate = () => {
|
||||
emit('resetTemplate');
|
||||
};
|
||||
|
||||
const goBack = () => {
|
||||
emit('back');
|
||||
};
|
||||
|
||||
onMounted(initializeTemplateParameters);
|
||||
|
||||
watch(
|
||||
() => props.template,
|
||||
() => {
|
||||
initializeTemplateParameters();
|
||||
v$.value.$reset();
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
defineExpose({
|
||||
processedParams,
|
||||
hasVariables,
|
||||
hasMediaHeader,
|
||||
headerComponent,
|
||||
renderedTemplate,
|
||||
v$,
|
||||
updateMediaUrl,
|
||||
sendMessage,
|
||||
resetTemplate,
|
||||
goBack,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex flex-col gap-4 p-4 mb-4 rounded-lg bg-n-alpha-black2">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-sm font-medium text-n-slate-12">
|
||||
{{ template.name }}
|
||||
</h3>
|
||||
<span class="text-xs text-n-slate-11">
|
||||
{{ languageLabel }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="rounded-md">
|
||||
<div class="text-sm whitespace-pre-wrap text-n-slate-12">
|
||||
{{ renderedTemplate }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-n-slate-11">
|
||||
{{ categoryLabel }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hasVariables || hasMediaHeader">
|
||||
<div v-if="hasMediaHeader" class="mb-4">
|
||||
<p class="mb-2.5 text-sm font-semibold">
|
||||
{{
|
||||
$t('WHATSAPP_TEMPLATES.PARSER.MEDIA_HEADER_LABEL', {
|
||||
type: formatType,
|
||||
}) || `${formatType} Header`
|
||||
}}
|
||||
</p>
|
||||
<div class="flex items-center mb-2.5">
|
||||
<Input
|
||||
:model-value="processedParams.header?.media_url || ''"
|
||||
type="url"
|
||||
class="flex-1"
|
||||
:placeholder="
|
||||
t('WHATSAPP_TEMPLATES.PARSER.MEDIA_URL_LABEL', {
|
||||
type: formatType,
|
||||
})
|
||||
"
|
||||
@update:model-value="updateMediaUrl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Body Variables Section -->
|
||||
<div v-if="processedParams.body">
|
||||
<p class="mb-2.5 text-sm font-semibold">
|
||||
{{ $t('WHATSAPP_TEMPLATES.PARSER.VARIABLES_LABEL') }}
|
||||
</p>
|
||||
<div
|
||||
v-for="(variable, key) in processedParams.body"
|
||||
:key="`body-${key}`"
|
||||
class="flex items-center mb-2.5"
|
||||
>
|
||||
<Input
|
||||
v-model="processedParams.body[key]"
|
||||
type="text"
|
||||
class="flex-1"
|
||||
:placeholder="
|
||||
t('WHATSAPP_TEMPLATES.PARSER.VARIABLE_PLACEHOLDER', {
|
||||
variable: key,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Button Variables Section -->
|
||||
<div v-if="processedParams.buttons">
|
||||
<p class="mb-2.5 text-sm font-semibold">
|
||||
{{ t('WHATSAPP_TEMPLATES.PARSER.BUTTON_PARAMETERS') }}
|
||||
</p>
|
||||
<div
|
||||
v-for="(button, index) in processedParams.buttons"
|
||||
:key="`button-${index}`"
|
||||
class="flex items-center mb-2.5"
|
||||
>
|
||||
<Input
|
||||
v-model="processedParams.buttons[index].parameter"
|
||||
type="text"
|
||||
class="flex-1"
|
||||
:placeholder="t('WHATSAPP_TEMPLATES.PARSER.BUTTON_PARAMETER')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
v-if="v$.$dirty && v$.$invalid"
|
||||
class="p-2.5 text-center rounded-md bg-n-ruby-9/20 text-n-ruby-9"
|
||||
>
|
||||
{{ $t('WHATSAPP_TEMPLATES.PARSER.FORM_ERROR_MESSAGE') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<slot
|
||||
name="actions"
|
||||
:send-message="sendMessage"
|
||||
:reset-template="resetTemplate"
|
||||
:go-back="goBack"
|
||||
:is-valid="!v$.$invalid"
|
||||
:disabled="isFormInvalid"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
+36
-143
@@ -1,4 +1,4 @@
|
||||
<script>
|
||||
<script setup>
|
||||
/**
|
||||
* This component handles parsing and sending WhatsApp message templates.
|
||||
* It works as follows:
|
||||
@@ -8,158 +8,51 @@
|
||||
* 4. Replaces placeholders with user-provided values.
|
||||
* 5. Emits events to send the processed message or reset the template.
|
||||
*/
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { requiredIf } from '@vuelidate/validators';
|
||||
|
||||
import WhatsAppTemplateParser from 'dashboard/components-next/whatsapp/WhatsAppTemplateParser.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NextButton,
|
||||
defineProps({
|
||||
template: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
props: {
|
||||
template: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
emits: ['sendMessage', 'resetTemplate'],
|
||||
setup(props, { emit }) {
|
||||
const processVariable = str => {
|
||||
return str.replace(/{{|}}/g, '');
|
||||
};
|
||||
});
|
||||
|
||||
const allKeysRequired = value => {
|
||||
const keys = Object.keys(value);
|
||||
return keys.every(key => value[key]);
|
||||
};
|
||||
const emit = defineEmits(['sendMessage', 'resetTemplate']);
|
||||
|
||||
const processedParams = ref({});
|
||||
const handleSendMessage = payload => {
|
||||
emit('sendMessage', payload);
|
||||
};
|
||||
|
||||
const templateString = computed(() => {
|
||||
return props.template.components.find(
|
||||
component => component.type === 'BODY'
|
||||
).text;
|
||||
});
|
||||
|
||||
const variables = computed(() => {
|
||||
return templateString.value.match(/{{([^}]+)}}/g);
|
||||
});
|
||||
|
||||
const processedString = computed(() => {
|
||||
return templateString.value.replace(/{{([^}]+)}}/g, (match, variable) => {
|
||||
const variableKey = processVariable(variable);
|
||||
return processedParams.value[variableKey] || `{{${variable}}}`;
|
||||
});
|
||||
});
|
||||
|
||||
const v$ = useVuelidate(
|
||||
{
|
||||
processedParams: {
|
||||
requiredIfKeysPresent: requiredIf(variables),
|
||||
allKeysRequired,
|
||||
},
|
||||
},
|
||||
{ processedParams }
|
||||
);
|
||||
|
||||
const generateVariables = () => {
|
||||
const matchedVariables = templateString.value.match(/{{([^}]+)}}/g);
|
||||
if (!matchedVariables) return;
|
||||
|
||||
const finalVars = matchedVariables.map(i => processVariable(i));
|
||||
processedParams.value = finalVars.reduce((acc, variable) => {
|
||||
acc[variable] = '';
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const resetTemplate = () => {
|
||||
emit('resetTemplate');
|
||||
};
|
||||
|
||||
const sendMessage = () => {
|
||||
v$.value.$touch();
|
||||
if (v$.value.$invalid) return;
|
||||
|
||||
const payload = {
|
||||
message: processedString.value,
|
||||
templateParams: {
|
||||
name: props.template.name,
|
||||
category: props.template.category,
|
||||
language: props.template.language,
|
||||
namespace: props.template.namespace,
|
||||
processed_params: processedParams.value,
|
||||
},
|
||||
};
|
||||
emit('sendMessage', payload);
|
||||
};
|
||||
|
||||
onMounted(generateVariables);
|
||||
|
||||
return {
|
||||
processedParams,
|
||||
variables,
|
||||
templateString,
|
||||
processedString,
|
||||
v$,
|
||||
resetTemplate,
|
||||
sendMessage,
|
||||
};
|
||||
},
|
||||
const handleResetTemplate = () => {
|
||||
emit('resetTemplate');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<textarea
|
||||
v-model="processedString"
|
||||
rows="4"
|
||||
readonly
|
||||
class="template-input"
|
||||
/>
|
||||
<div v-if="variables" class="p-2.5">
|
||||
<p class="text-sm font-semibold mb-2.5">
|
||||
{{ $t('WHATSAPP_TEMPLATES.PARSER.VARIABLES_LABEL') }}
|
||||
</p>
|
||||
<div
|
||||
v-for="(variable, key) in processedParams"
|
||||
:key="key"
|
||||
class="items-center flex mb-2.5"
|
||||
>
|
||||
<span
|
||||
class="bg-n-alpha-black2 text-n-slate-12 inline-block rounded-md text-xs py-2.5 px-6"
|
||||
>
|
||||
{{ key }}
|
||||
</span>
|
||||
<woot-input
|
||||
v-model="processedParams[key]"
|
||||
type="text"
|
||||
class="flex-1 text-sm ml-2.5"
|
||||
:styles="{ marginBottom: 0 }"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
v-if="v$.$dirty && v$.$invalid"
|
||||
class="bg-n-ruby-9/20 rounded-md text-n-ruby-9 p-2.5 text-center"
|
||||
>
|
||||
{{ $t('WHATSAPP_TEMPLATES.PARSER.FORM_ERROR_MESSAGE') }}
|
||||
</p>
|
||||
</div>
|
||||
<footer class="flex justify-end gap-2">
|
||||
<NextButton
|
||||
faded
|
||||
slate
|
||||
type="reset"
|
||||
:label="$t('WHATSAPP_TEMPLATES.PARSER.GO_BACK_LABEL')"
|
||||
@click="resetTemplate"
|
||||
/>
|
||||
<NextButton
|
||||
type="button"
|
||||
:label="$t('WHATSAPP_TEMPLATES.PARSER.SEND_MESSAGE_LABEL')"
|
||||
@click="sendMessage"
|
||||
/>
|
||||
</footer>
|
||||
<WhatsAppTemplateParser
|
||||
:template="template"
|
||||
@send-message="handleSendMessage"
|
||||
@reset-template="handleResetTemplate"
|
||||
>
|
||||
<template #actions="{ sendMessage, resetTemplate, disabled }">
|
||||
<footer class="flex gap-2 justify-end">
|
||||
<NextButton
|
||||
faded
|
||||
slate
|
||||
type="reset"
|
||||
:label="$t('WHATSAPP_TEMPLATES.PARSER.GO_BACK_LABEL')"
|
||||
@click="resetTemplate"
|
||||
/>
|
||||
<NextButton
|
||||
type="button"
|
||||
:label="$t('WHATSAPP_TEMPLATES.PARSER.SEND_MESSAGE_LABEL')"
|
||||
:disabled="disabled"
|
||||
@click="sendMessage"
|
||||
/>
|
||||
</footer>
|
||||
</template>
|
||||
</WhatsAppTemplateParser>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+130
-67
@@ -1,60 +1,71 @@
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref, computed, toRef } from 'vue';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useFunctionGetter, useStore } from 'dashboard/composables/store';
|
||||
import {
|
||||
COMPONENT_TYPES,
|
||||
MEDIA_FORMATS,
|
||||
findComponentByType,
|
||||
} from 'dashboard/helper/templateHelper';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
// TODO: Remove this when we support all formats
|
||||
const formatsToRemove = ['DOCUMENT', 'IMAGE', 'VIDEO'];
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
},
|
||||
props: {
|
||||
inboxId: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
emits: ['onSelect'],
|
||||
data() {
|
||||
return {
|
||||
query: '',
|
||||
isRefreshing: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
whatsAppTemplateMessages() {
|
||||
// TODO: Remove the last filter when we support all formats
|
||||
return this.$store.getters['inboxes/getWhatsAppTemplates'](this.inboxId)
|
||||
.filter(template => template.status.toLowerCase() === 'approved')
|
||||
.filter(template => {
|
||||
return template.components.every(component => {
|
||||
return !formatsToRemove.includes(component.format);
|
||||
});
|
||||
});
|
||||
},
|
||||
filteredTemplateMessages() {
|
||||
return this.whatsAppTemplateMessages.filter(template =>
|
||||
template.name.toLowerCase().includes(this.query.toLowerCase())
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getTemplatebody(template) {
|
||||
return template.components.find(component => component.type === 'BODY')
|
||||
.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;
|
||||
}
|
||||
},
|
||||
const props = defineProps({
|
||||
inboxId: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['onSelect']);
|
||||
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
const query = ref('');
|
||||
const isRefreshing = ref(false);
|
||||
|
||||
const whatsAppTemplateMessages = useFunctionGetter(
|
||||
'inboxes/getFilteredWhatsAppTemplates',
|
||||
toRef(props, 'inboxId')
|
||||
);
|
||||
|
||||
const filteredTemplateMessages = computed(() =>
|
||||
whatsAppTemplateMessages.value.filter(template =>
|
||||
template.name.toLowerCase().includes(query.value.toLowerCase())
|
||||
)
|
||||
);
|
||||
|
||||
const getTemplateBody = template => {
|
||||
return findComponentByType(template, COMPONENT_TYPES.BODY)?.text || '';
|
||||
};
|
||||
|
||||
const getTemplateHeader = template => {
|
||||
return findComponentByType(template, COMPONENT_TYPES.HEADER);
|
||||
};
|
||||
|
||||
const getTemplateFooter = template => {
|
||||
return findComponentByType(template, COMPONENT_TYPES.FOOTER);
|
||||
};
|
||||
|
||||
const getTemplateButtons = template => {
|
||||
return findComponentByType(template, COMPONENT_TYPES.BUTTONS);
|
||||
};
|
||||
|
||||
const hasMediaContent = template => {
|
||||
const header = getTemplateHeader(template);
|
||||
return header && MEDIA_FORMATS.includes(header.format);
|
||||
};
|
||||
|
||||
const refreshTemplates = async () => {
|
||||
isRefreshing.value = true;
|
||||
try {
|
||||
await store.dispatch('inboxes/syncTemplates', props.inboxId);
|
||||
useAlert(t('WHATSAPP_TEMPLATES.PICKER.REFRESH_SUCCESS'));
|
||||
} catch (error) {
|
||||
useAlert(t('WHATSAPP_TEMPLATES.PICKER.REFRESH_ERROR'));
|
||||
} finally {
|
||||
isRefreshing.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -68,14 +79,14 @@ export default {
|
||||
<input
|
||||
v-model="query"
|
||||
type="search"
|
||||
:placeholder="$t('WHATSAPP_TEMPLATES.PICKER.SEARCH_PLACEHOLDER')"
|
||||
: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')"
|
||||
:title="t('WHATSAPP_TEMPLATES.PICKER.REFRESH_BUTTON')"
|
||||
@click="refreshTemplates"
|
||||
>
|
||||
<Icon
|
||||
@@ -91,7 +102,7 @@ export default {
|
||||
<div v-for="(template, i) in filteredTemplateMessages" :key="template.id">
|
||||
<button
|
||||
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 class="flex justify-between items-center mb-2.5">
|
||||
@@ -101,21 +112,73 @@ export default {
|
||||
<span
|
||||
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 }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium">
|
||||
{{ $t('WHATSAPP_TEMPLATES.PICKER.LABELS.TEMPLATE_BODY') }}
|
||||
<!-- Header -->
|
||||
<div v-if="getTemplateHeader(template)" class="mb-3">
|
||||
<p class="text-xs font-medium text-n-slate-11">
|
||||
{{ t('WHATSAPP_TEMPLATES.PICKER.HEADER') || 'HEADER' }}
|
||||
</p>
|
||||
<p class="label-body">{{ getTemplatebody(template) }}</p>
|
||||
<div
|
||||
v-if="getTemplateHeader(template).format === 'TEXT'"
|
||||
class="text-sm label-body"
|
||||
>
|
||||
{{ getTemplateHeader(template).text }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="hasMediaContent(template)"
|
||||
class="text-sm italic text-n-slate-11"
|
||||
>
|
||||
{{
|
||||
t('WHATSAPP_TEMPLATES.PICKER.MEDIA_CONTENT', {
|
||||
format: getTemplateHeader(template).format,
|
||||
}) ||
|
||||
`${getTemplateHeader(template).format} ${t('WHATSAPP_TEMPLATES.PICKER.MEDIA_CONTENT_FALLBACK')}`
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5">
|
||||
<p class="font-medium">
|
||||
{{ $t('WHATSAPP_TEMPLATES.PICKER.LABELS.CATEGORY') }}
|
||||
|
||||
<!-- Body -->
|
||||
<div>
|
||||
<p class="text-xs font-medium text-n-slate-11">
|
||||
{{ t('WHATSAPP_TEMPLATES.PICKER.BODY') || 'BODY' }}
|
||||
</p>
|
||||
<p>{{ template.category }}</p>
|
||||
<p class="text-sm label-body">{{ getTemplateBody(template) }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div v-if="getTemplateFooter(template)" class="mt-3">
|
||||
<p class="text-xs font-medium text-n-slate-11">
|
||||
{{ t('WHATSAPP_TEMPLATES.PICKER.FOOTER') || 'FOOTER' }}
|
||||
</p>
|
||||
<p class="text-sm label-body">
|
||||
{{ getTemplateFooter(template).text }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div v-if="getTemplateButtons(template)" class="mt-3">
|
||||
<p class="text-xs font-medium text-n-slate-11">
|
||||
{{ t('WHATSAPP_TEMPLATES.PICKER.BUTTONS') || 'BUTTONS' }}
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-1 mt-1">
|
||||
<span
|
||||
v-for="button in getTemplateButtons(template).buttons"
|
||||
:key="button.text"
|
||||
class="px-2 py-1 text-xs rounded bg-n-slate-3 text-n-slate-12"
|
||||
>
|
||||
{{ button.text }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<p class="text-xs font-medium text-n-slate-11">
|
||||
{{ t('WHATSAPP_TEMPLATES.PICKER.CATEGORY') || 'CATEGORY' }}
|
||||
</p>
|
||||
<p class="text-sm">{{ template.category }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
@@ -128,13 +191,13 @@ export default {
|
||||
<div v-if="!filteredTemplateMessages.length" class="py-8 text-center">
|
||||
<div v-if="query && whatsAppTemplateMessages.length">
|
||||
<p>
|
||||
{{ $t('WHATSAPP_TEMPLATES.PICKER.NO_TEMPLATES_FOUND') }}
|
||||
{{ t('WHATSAPP_TEMPLATES.PICKER.NO_TEMPLATES_FOUND') }}
|
||||
<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') }}
|
||||
{{ t('WHATSAPP_TEMPLATES.PICKER.NO_TEMPLATES_AVAILABLE') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -196,6 +196,7 @@ describe('useAutomation', () => {
|
||||
automationTypes.conversation_created = { conditions: [] };
|
||||
automationTypes.conversation_updated = { conditions: [] };
|
||||
automationTypes.conversation_opened = { conditions: [] };
|
||||
automationTypes.conversation_resolved = { conditions: [] };
|
||||
|
||||
automationHelper.generateCustomAttributeTypes.mockReturnValue([]);
|
||||
automationHelper.generateCustomAttributes.mockReturnValue([]);
|
||||
|
||||
@@ -8,7 +8,7 @@ export const DEFAULT_MESSAGE_CREATED_CONDITION = [
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_CONVERSATION_OPENED_CONDITION = [
|
||||
export const DEFAULT_CONVERSATION_CONDITION = [
|
||||
{
|
||||
attribute_key: 'browser_language',
|
||||
filter_operator: 'equal_to',
|
||||
|
||||
@@ -37,7 +37,6 @@ export const FEATURE_FLAGS = {
|
||||
REPORT_V4: 'report_v4',
|
||||
CHANNEL_INSTAGRAM: 'channel_instagram',
|
||||
CONTACT_CHATWOOT_SUPPORT_TEAM: 'contact_chatwoot_support_team',
|
||||
WHATSAPP_EMBEDDED_SIGNUP: 'whatsapp_embedded_signup',
|
||||
CAPTAIN_V2: 'captain_integration_v2',
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from 'dashboard/routes/dashboard/settings/automation/operators';
|
||||
import {
|
||||
DEFAULT_MESSAGE_CREATED_CONDITION,
|
||||
DEFAULT_CONVERSATION_OPENED_CONDITION,
|
||||
DEFAULT_CONVERSATION_CONDITION,
|
||||
DEFAULT_OTHER_CONDITION,
|
||||
DEFAULT_ACTIONS,
|
||||
} from 'dashboard/constants/automation';
|
||||
@@ -169,8 +169,11 @@ export const getDefaultConditions = eventName => {
|
||||
if (eventName === 'message_created') {
|
||||
return DEFAULT_MESSAGE_CREATED_CONDITION;
|
||||
}
|
||||
if (eventName === 'conversation_opened') {
|
||||
return DEFAULT_CONVERSATION_OPENED_CONDITION;
|
||||
if (
|
||||
eventName === 'conversation_opened' ||
|
||||
eventName === 'conversation_resolved'
|
||||
) {
|
||||
return DEFAULT_CONVERSATION_CONDITION;
|
||||
}
|
||||
return DEFAULT_OTHER_CONDITION;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
import {
|
||||
replaceTemplateVariables,
|
||||
buildTemplateParameters,
|
||||
processVariable,
|
||||
allKeysRequired,
|
||||
} from '../templateHelper';
|
||||
import { templates } from '../../store/modules/specs/inboxes/templateFixtures';
|
||||
|
||||
describe('templateHelper', () => {
|
||||
const technicianTemplate = templates.find(t => t.name === 'technician_visit');
|
||||
|
||||
describe('processVariable', () => {
|
||||
it('should remove curly braces from variables', () => {
|
||||
expect(processVariable('{{name}}')).toBe('name');
|
||||
expect(processVariable('{{1}}')).toBe('1');
|
||||
expect(processVariable('{{customer_id}}')).toBe('customer_id');
|
||||
});
|
||||
});
|
||||
|
||||
describe('allKeysRequired', () => {
|
||||
it('should return true when all keys have values', () => {
|
||||
const obj = { name: 'John', age: '30' };
|
||||
expect(allKeysRequired(obj)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when some keys are empty', () => {
|
||||
const obj = { name: 'John', age: '' };
|
||||
expect(allKeysRequired(obj)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for empty object', () => {
|
||||
expect(allKeysRequired({})).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('replaceTemplateVariables', () => {
|
||||
const templateText =
|
||||
"Hi {{1}}, we're scheduling a technician visit to {{2}} on {{3}} between {{4}} and {{5}}. Please confirm if this time slot works for you.";
|
||||
|
||||
it('should replace all variables with provided values', () => {
|
||||
const processedParams = {
|
||||
body: {
|
||||
1: 'John',
|
||||
2: '123 Main St',
|
||||
3: '2025-01-15',
|
||||
4: '10:00 AM',
|
||||
5: '2:00 PM',
|
||||
},
|
||||
};
|
||||
|
||||
const result = replaceTemplateVariables(templateText, processedParams);
|
||||
expect(result).toBe(
|
||||
"Hi John, we're scheduling a technician visit to 123 Main St on 2025-01-15 between 10:00 AM and 2:00 PM. Please confirm if this time slot works for you."
|
||||
);
|
||||
});
|
||||
|
||||
it('should keep original variable format when no replacement value provided', () => {
|
||||
const processedParams = {
|
||||
body: {
|
||||
1: 'John',
|
||||
3: '2025-01-15',
|
||||
},
|
||||
};
|
||||
|
||||
const result = replaceTemplateVariables(templateText, processedParams);
|
||||
expect(result).toContain('John');
|
||||
expect(result).toContain('2025-01-15');
|
||||
expect(result).toContain('{{2}}');
|
||||
expect(result).toContain('{{4}}');
|
||||
expect(result).toContain('{{5}}');
|
||||
});
|
||||
|
||||
it('should handle empty processedParams', () => {
|
||||
const result = replaceTemplateVariables(templateText, {});
|
||||
expect(result).toBe(templateText);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildTemplateParameters', () => {
|
||||
it('should build parameters for template with body variables', () => {
|
||||
const result = buildTemplateParameters(technicianTemplate, false);
|
||||
|
||||
expect(result.body).toEqual({
|
||||
1: '',
|
||||
2: '',
|
||||
3: '',
|
||||
4: '',
|
||||
5: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should include header parameters when hasMediaHeader is true', () => {
|
||||
const imageTemplate = templates.find(
|
||||
t => t.name === 'order_confirmation'
|
||||
);
|
||||
const result = buildTemplateParameters(imageTemplate, true);
|
||||
|
||||
expect(result.header).toEqual({
|
||||
media_url: '',
|
||||
media_type: 'image',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not include header parameters when hasMediaHeader is false', () => {
|
||||
const result = buildTemplateParameters(technicianTemplate, false);
|
||||
expect(result.header).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle template with no body component', () => {
|
||||
const templateWithoutBody = {
|
||||
components: [{ type: 'HEADER', format: 'TEXT' }],
|
||||
};
|
||||
|
||||
const result = buildTemplateParameters(templateWithoutBody, false);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle template with no variables', () => {
|
||||
const templateWithoutVars = templates.find(
|
||||
t => t.name === 'no_variable_template'
|
||||
);
|
||||
const result = buildTemplateParameters(templateWithoutVars, false);
|
||||
|
||||
expect(result.body).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle URL buttons with variables for non-authentication templates', () => {
|
||||
const templateWithUrlButton = {
|
||||
category: 'MARKETING',
|
||||
components: [
|
||||
{
|
||||
type: 'BODY',
|
||||
text: 'Check out our website at {{site_url}}',
|
||||
},
|
||||
{
|
||||
type: 'BUTTONS',
|
||||
buttons: [
|
||||
{
|
||||
type: 'URL',
|
||||
url: 'https://example.com/{{campaign_id}}',
|
||||
text: 'Visit Site',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = buildTemplateParameters(templateWithUrlButton, false);
|
||||
expect(result.buttons).toEqual([
|
||||
{
|
||||
type: 'url',
|
||||
parameter: '',
|
||||
url: 'https://example.com/{{campaign_id}}',
|
||||
variables: ['campaign_id'],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle templates with no variables', () => {
|
||||
const emptyTemplate = templates.find(
|
||||
t => t.name === 'no_variable_template'
|
||||
);
|
||||
const result = buildTemplateParameters(emptyTemplate, false);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should build parameters for templates with multiple component types', () => {
|
||||
const complexTemplate = {
|
||||
components: [
|
||||
{ type: 'HEADER', format: 'IMAGE' },
|
||||
{ type: 'BODY', text: 'Hi {{1}}, your order {{2}} is ready!' },
|
||||
{ type: 'FOOTER', text: 'Thank you for your business' },
|
||||
{
|
||||
type: 'BUTTONS',
|
||||
buttons: [{ type: 'URL', url: 'https://example.com/{{3}}' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = buildTemplateParameters(complexTemplate, true);
|
||||
|
||||
expect(result.header).toEqual({
|
||||
media_url: '',
|
||||
media_type: 'image',
|
||||
});
|
||||
expect(result.body).toEqual({ 1: '', 2: '' });
|
||||
expect(result.buttons).toEqual([
|
||||
{
|
||||
type: 'url',
|
||||
parameter: '',
|
||||
url: 'https://example.com/{{3}}',
|
||||
variables: ['3'],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle copy code buttons correctly', () => {
|
||||
const copyCodeTemplate = templates.find(
|
||||
t => t.name === 'discount_coupon'
|
||||
);
|
||||
const result = buildTemplateParameters(copyCodeTemplate, false);
|
||||
|
||||
expect(result.body).toBeDefined();
|
||||
expect(result.buttons).toEqual([
|
||||
{
|
||||
type: 'copy_code',
|
||||
parameter: '',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle templates with document headers', () => {
|
||||
const documentTemplate = templates.find(
|
||||
t => t.name === 'purchase_receipt'
|
||||
);
|
||||
const result = buildTemplateParameters(documentTemplate, true);
|
||||
|
||||
expect(result.header).toEqual({
|
||||
media_url: '',
|
||||
media_type: 'document',
|
||||
});
|
||||
expect(result.body).toEqual({
|
||||
1: '',
|
||||
2: '',
|
||||
3: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle video header templates', () => {
|
||||
const videoTemplate = templates.find(t => t.name === 'training_video');
|
||||
const result = buildTemplateParameters(videoTemplate, true);
|
||||
|
||||
expect(result.header).toEqual({
|
||||
media_url: '',
|
||||
media_type: 'video',
|
||||
});
|
||||
expect(result.body).toEqual({
|
||||
name: '',
|
||||
date: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('enhanced format validation', () => {
|
||||
it('should validate enhanced format structure', () => {
|
||||
const processedParams = {
|
||||
body: { 1: 'John', 2: 'Order123' },
|
||||
header: {
|
||||
media_url: 'https://example.com/image.jpg',
|
||||
media_type: 'image',
|
||||
},
|
||||
buttons: [{ type: 'copy_code', parameter: 'SAVE20' }],
|
||||
};
|
||||
|
||||
// Test that structure is properly formed
|
||||
expect(processedParams.body).toBeDefined();
|
||||
expect(typeof processedParams.body).toBe('object');
|
||||
expect(processedParams.header).toBeDefined();
|
||||
expect(Array.isArray(processedParams.buttons)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle empty component sections', () => {
|
||||
const processedParams = {
|
||||
body: {},
|
||||
header: {},
|
||||
buttons: [],
|
||||
};
|
||||
|
||||
expect(allKeysRequired(processedParams.body)).toBe(true);
|
||||
expect(allKeysRequired(processedParams.header)).toBe(true);
|
||||
expect(processedParams.buttons.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should validate parameter completeness', () => {
|
||||
const incompleteParams = {
|
||||
body: { 1: 'John', 2: '' },
|
||||
};
|
||||
|
||||
expect(allKeysRequired(incompleteParams.body)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle edge cases in processVariable', () => {
|
||||
expect(processVariable('{{')).toBe('');
|
||||
expect(processVariable('}}')).toBe('');
|
||||
expect(processVariable('')).toBe('');
|
||||
expect(processVariable('{{nested{{variable}}}}')).toBe('nestedvariable');
|
||||
});
|
||||
|
||||
it('should handle special characters in template variables', () => {
|
||||
/* eslint-disable no-template-curly-in-string */
|
||||
const templateText =
|
||||
'Welcome {{user_name}}, your order #{{order_id}} costs ${{amount}}';
|
||||
/* eslint-enable no-template-curly-in-string */
|
||||
const processedParams = {
|
||||
body: {
|
||||
user_name: 'John & Jane',
|
||||
order_id: '12345',
|
||||
amount: '99.99',
|
||||
},
|
||||
};
|
||||
|
||||
const result = replaceTemplateVariables(templateText, processedParams);
|
||||
expect(result).toBe(
|
||||
'Welcome John & Jane, your order #12345 costs $99.99'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle templates with mixed parameter types', () => {
|
||||
const mixedTemplate = {
|
||||
components: [
|
||||
{ type: 'HEADER', format: 'VIDEO' },
|
||||
{ type: 'BODY', text: 'Order {{order_id}} status: {{status}}' },
|
||||
{ type: 'FOOTER', text: 'Thank you' },
|
||||
{
|
||||
type: 'BUTTONS',
|
||||
buttons: [
|
||||
{ type: 'URL', url: 'https://track.com/{{order_id}}' },
|
||||
{ type: 'COPY_CODE' },
|
||||
{ type: 'PHONE_NUMBER', phone_number: '+1234567890' },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = buildTemplateParameters(mixedTemplate, true);
|
||||
|
||||
expect(result.header).toEqual({
|
||||
media_url: '',
|
||||
media_type: 'video',
|
||||
});
|
||||
expect(result.body).toEqual({
|
||||
order_id: '',
|
||||
status: '',
|
||||
});
|
||||
expect(result.buttons).toHaveLength(2); // URL and COPY_CODE (PHONE_NUMBER doesn't need parameters)
|
||||
expect(result.buttons[0].type).toBe('url');
|
||||
expect(result.buttons[1].type).toBe('copy_code');
|
||||
});
|
||||
|
||||
it('should handle templates with no processable components', () => {
|
||||
const emptyTemplate = {
|
||||
components: [
|
||||
{ type: 'HEADER', format: 'TEXT', text: 'Static Header' },
|
||||
{ type: 'BODY', text: 'Static body with no variables' },
|
||||
{ type: 'FOOTER', text: 'Static footer' },
|
||||
],
|
||||
};
|
||||
|
||||
const result = buildTemplateParameters(emptyTemplate, false);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should validate that replaceTemplateVariables preserves unreplaced variables', () => {
|
||||
const templateText = 'Hi {{name}}, order {{order_id}} is {{status}}';
|
||||
const partialParams = {
|
||||
body: {
|
||||
name: 'John',
|
||||
// order_id missing
|
||||
status: 'ready',
|
||||
},
|
||||
};
|
||||
|
||||
const result = replaceTemplateVariables(templateText, partialParams);
|
||||
expect(result).toBe('Hi John, order {{order_id}} is ready');
|
||||
expect(result).toContain('{{order_id}}'); // Unreplaced variable preserved
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
// Constants
|
||||
export const DEFAULT_LANGUAGE = 'en';
|
||||
export const DEFAULT_CATEGORY = 'UTILITY';
|
||||
export const COMPONENT_TYPES = {
|
||||
HEADER: 'HEADER',
|
||||
BODY: 'BODY',
|
||||
BUTTONS: 'BUTTONS',
|
||||
};
|
||||
export const MEDIA_FORMATS = ['IMAGE', 'VIDEO', 'DOCUMENT'];
|
||||
|
||||
export const findComponentByType = (template, type) =>
|
||||
template.components?.find(component => component.type === type);
|
||||
|
||||
export const processVariable = str => {
|
||||
return str.replace(/{{|}}/g, '');
|
||||
};
|
||||
|
||||
export const allKeysRequired = value => {
|
||||
const keys = Object.keys(value);
|
||||
return keys.every(key => value[key]);
|
||||
};
|
||||
|
||||
export const replaceTemplateVariables = (templateText, processedParams) => {
|
||||
return templateText.replace(/{{([^}]+)}}/g, (match, variable) => {
|
||||
const variableKey = processVariable(variable);
|
||||
return processedParams.body?.[variableKey] || `{{${variable}}}`;
|
||||
});
|
||||
};
|
||||
|
||||
export const buildTemplateParameters = (template, hasMediaHeaderValue) => {
|
||||
const allVariables = {};
|
||||
|
||||
const bodyComponent = findComponentByType(template, COMPONENT_TYPES.BODY);
|
||||
const headerComponent = findComponentByType(template, COMPONENT_TYPES.HEADER);
|
||||
|
||||
if (!bodyComponent) return allVariables;
|
||||
|
||||
const templateString = bodyComponent.text;
|
||||
|
||||
// Process body variables
|
||||
const matchedVariables = templateString.match(/{{([^}]+)}}/g);
|
||||
if (matchedVariables) {
|
||||
allVariables.body = {};
|
||||
matchedVariables.forEach(variable => {
|
||||
const key = processVariable(variable);
|
||||
allVariables.body[key] = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (hasMediaHeaderValue) {
|
||||
if (!allVariables.header) allVariables.header = {};
|
||||
allVariables.header.media_url = '';
|
||||
allVariables.header.media_type = headerComponent.format.toLowerCase();
|
||||
}
|
||||
|
||||
// Process button variables
|
||||
const buttonComponents = template.components.filter(
|
||||
component => component.type === COMPONENT_TYPES.BUTTONS
|
||||
);
|
||||
|
||||
buttonComponents.forEach(buttonComponent => {
|
||||
if (buttonComponent.buttons) {
|
||||
buttonComponent.buttons.forEach((button, index) => {
|
||||
// Handle URL buttons with variables
|
||||
if (button.type === 'URL' && button.url && button.url.includes('{{')) {
|
||||
const buttonVars = button.url.match(/{{([^}]+)}}/g) || [];
|
||||
if (buttonVars.length > 0) {
|
||||
if (!allVariables.buttons) allVariables.buttons = [];
|
||||
allVariables.buttons[index] = {
|
||||
type: 'url',
|
||||
parameter: '',
|
||||
url: button.url,
|
||||
variables: buttonVars.map(v => processVariable(v)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Handle copy code buttons
|
||||
if (button.type === 'COPY_CODE') {
|
||||
if (!allVariables.buttons) allVariables.buttons = [];
|
||||
allVariables.buttons[index] = {
|
||||
type: 'copy_code',
|
||||
parameter: '',
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return allVariables;
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import ar from './locale/ar';
|
||||
import bg from './locale/bg';
|
||||
import ca from './locale/ca';
|
||||
import cs from './locale/cs';
|
||||
import da from './locale/da';
|
||||
@@ -40,6 +41,7 @@ import lt from './locale/lt';
|
||||
|
||||
export default {
|
||||
ar,
|
||||
bg,
|
||||
ca,
|
||||
cs,
|
||||
da,
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
"CONVERSATION_CREATED": "Conversation Created",
|
||||
"CONVERSATION_UPDATED": "Conversation Updated",
|
||||
"MESSAGE_CREATED": "Message Created",
|
||||
"CONVERSATION_RESOLVED": "Conversation Resolved",
|
||||
"CONVERSATION_OPENED": "Conversation Opened"
|
||||
},
|
||||
"ACTIONS": {
|
||||
|
||||
@@ -472,7 +472,8 @@
|
||||
"FEATURES": {
|
||||
"TITLE": "Features",
|
||||
"ALLOW_CONVERSATION_FAQS": "Generate FAQs from resolved conversations",
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions."
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions.",
|
||||
"ALLOW_CITATIONS": "Include source citations in responses"
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
|
||||
@@ -1,29 +1,46 @@
|
||||
{
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "Whatsapp Templates",
|
||||
"SUBTITLE": "Select the whatsapp template you want to send",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Process {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "Search Templates",
|
||||
"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": {
|
||||
"LANGUAGE": "Language",
|
||||
"TEMPLATE_BODY": "Template Body",
|
||||
"CATEGORY": "Category"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "Variables",
|
||||
"VARIABLE_PLACEHOLDER": "Enter {variable} value",
|
||||
"GO_BACK_LABEL": "Go Back",
|
||||
"SEND_MESSAGE_LABEL": "Send Message",
|
||||
"FORM_ERROR_MESSAGE": "Please fill all variables before sending"
|
||||
}
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "Whatsapp Templates",
|
||||
"SUBTITLE": "Select the whatsapp template you want to send",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Configure template: {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "Search Templates",
|
||||
"NO_TEMPLATES_FOUND": "No templates found for",
|
||||
"HEADER": "Header",
|
||||
"BODY": "Body",
|
||||
"FOOTER": "Footer",
|
||||
"BUTTONS": "Buttons",
|
||||
"CATEGORY": "Category",
|
||||
"MEDIA_CONTENT": "Media Content",
|
||||
"MEDIA_CONTENT_FALLBACK": "media content",
|
||||
"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": {
|
||||
"LANGUAGE": "Language",
|
||||
"TEMPLATE_BODY": "Template Body",
|
||||
"CATEGORY": "Category"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "Variables",
|
||||
"LANGUAGE": "Language",
|
||||
"CATEGORY": "Category",
|
||||
"VARIABLE_PLACEHOLDER": "Enter {variable} value",
|
||||
"GO_BACK_LABEL": "Go Back",
|
||||
"SEND_MESSAGE_LABEL": "Send Message",
|
||||
"FORM_ERROR_MESSAGE": "Please fill all variables before sending",
|
||||
"MEDIA_HEADER_LABEL": "{type} Header",
|
||||
"OTP_CODE": "Enter 4-8 digit OTP",
|
||||
"EXPIRY_MINUTES": "Enter expiry minutes",
|
||||
"BUTTON_PARAMETERS": "Button Parameters",
|
||||
"BUTTON_LABEL": "Button {index}",
|
||||
"COUPON_CODE": "Enter coupon code (max 15 chars)",
|
||||
"MEDIA_URL_LABEL": "Enter {type} URL",
|
||||
"BUTTON_PARAMETER": "Enter button parameter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
"CONVERSATION_CREATED": "تم إنشاء المحادثة",
|
||||
"CONVERSATION_UPDATED": "تم تحديث المحادثة",
|
||||
"MESSAGE_CREATED": "Message Created",
|
||||
"CONVERSATION_RESOLVED": "Conversation Resolved",
|
||||
"CONVERSATION_OPENED": "Conversation Opened"
|
||||
},
|
||||
"ACTIONS": {
|
||||
|
||||
@@ -472,7 +472,8 @@
|
||||
"FEATURES": {
|
||||
"TITLE": "الخصائص",
|
||||
"ALLOW_CONVERSATION_FAQS": "Generate FAQs from resolved conversations",
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions."
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions.",
|
||||
"ALLOW_CITATIONS": "Include source citations in responses"
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
|
||||
@@ -1,29 +1,46 @@
|
||||
{
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "قوالب الواتساب",
|
||||
"SUBTITLE": "حدد القالب الذي تريد إرساله",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "معالجة {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "نماذج البحث",
|
||||
"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": {
|
||||
"LANGUAGE": "اللغة",
|
||||
"TEMPLATE_BODY": "نص القالب",
|
||||
"CATEGORY": "الفئة"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "المتغيرات",
|
||||
"VARIABLE_PLACEHOLDER": "أدخل قيمة {variable}",
|
||||
"GO_BACK_LABEL": "العودة للخلف",
|
||||
"SEND_MESSAGE_LABEL": "إرسال الرسالة",
|
||||
"FORM_ERROR_MESSAGE": "يرجى ملء جميع المتغيرات قبل الإرسال"
|
||||
}
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "قوالب الواتساب",
|
||||
"SUBTITLE": "حدد القالب الذي تريد إرساله",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Configure template: {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "نماذج البحث",
|
||||
"NO_TEMPLATES_FOUND": "لم يتم العثور على قوالب",
|
||||
"HEADER": "Header",
|
||||
"BODY": "Body",
|
||||
"FOOTER": "Footer",
|
||||
"BUTTONS": "Buttons",
|
||||
"CATEGORY": "الفئة",
|
||||
"MEDIA_CONTENT": "Media Content",
|
||||
"MEDIA_CONTENT_FALLBACK": "media content",
|
||||
"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": {
|
||||
"LANGUAGE": "اللغة",
|
||||
"TEMPLATE_BODY": "نص القالب",
|
||||
"CATEGORY": "الفئة"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "المتغيرات",
|
||||
"LANGUAGE": "اللغة",
|
||||
"CATEGORY": "الفئة",
|
||||
"VARIABLE_PLACEHOLDER": "أدخل قيمة {variable}",
|
||||
"GO_BACK_LABEL": "العودة للخلف",
|
||||
"SEND_MESSAGE_LABEL": "إرسال الرسالة",
|
||||
"FORM_ERROR_MESSAGE": "يرجى ملء جميع المتغيرات قبل الإرسال",
|
||||
"MEDIA_HEADER_LABEL": "{type} Header",
|
||||
"OTP_CODE": "Enter 4-8 digit OTP",
|
||||
"EXPIRY_MINUTES": "Enter expiry minutes",
|
||||
"BUTTON_PARAMETERS": "Button Parameters",
|
||||
"BUTTON_LABEL": "Button {index}",
|
||||
"COUPON_CODE": "Enter coupon code (max 15 chars)",
|
||||
"MEDIA_URL_LABEL": "Enter {type} URL",
|
||||
"BUTTON_PARAMETER": "Enter button parameter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
"CONVERSATION_CREATED": "Conversation Created",
|
||||
"CONVERSATION_UPDATED": "Conversation Updated",
|
||||
"MESSAGE_CREATED": "Message Created",
|
||||
"CONVERSATION_RESOLVED": "Conversation Resolved",
|
||||
"CONVERSATION_OPENED": "Conversation Opened"
|
||||
},
|
||||
"ACTIONS": {
|
||||
|
||||
@@ -472,7 +472,8 @@
|
||||
"FEATURES": {
|
||||
"TITLE": "Features",
|
||||
"ALLOW_CONVERSATION_FAQS": "Generate FAQs from resolved conversations",
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions."
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions.",
|
||||
"ALLOW_CITATIONS": "Include source citations in responses"
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
|
||||
@@ -1,29 +1,46 @@
|
||||
{
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "Whatsapp Templates",
|
||||
"SUBTITLE": "Select the whatsapp template you want to send",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Process {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "Search Templates",
|
||||
"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": {
|
||||
"LANGUAGE": "Language",
|
||||
"TEMPLATE_BODY": "Template Body",
|
||||
"CATEGORY": "Category"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "Variables",
|
||||
"VARIABLE_PLACEHOLDER": "Enter {variable} value",
|
||||
"GO_BACK_LABEL": "Go Back",
|
||||
"SEND_MESSAGE_LABEL": "Send Message",
|
||||
"FORM_ERROR_MESSAGE": "Please fill all variables before sending"
|
||||
}
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "Whatsapp Templates",
|
||||
"SUBTITLE": "Select the whatsapp template you want to send",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Configure template: {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "Search Templates",
|
||||
"NO_TEMPLATES_FOUND": "No templates found for",
|
||||
"HEADER": "Header",
|
||||
"BODY": "Body",
|
||||
"FOOTER": "Footer",
|
||||
"BUTTONS": "Buttons",
|
||||
"CATEGORY": "Category",
|
||||
"MEDIA_CONTENT": "Media Content",
|
||||
"MEDIA_CONTENT_FALLBACK": "media content",
|
||||
"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": {
|
||||
"LANGUAGE": "Language",
|
||||
"TEMPLATE_BODY": "Template Body",
|
||||
"CATEGORY": "Category"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "Variables",
|
||||
"LANGUAGE": "Language",
|
||||
"CATEGORY": "Category",
|
||||
"VARIABLE_PLACEHOLDER": "Enter {variable} value",
|
||||
"GO_BACK_LABEL": "Go Back",
|
||||
"SEND_MESSAGE_LABEL": "Send Message",
|
||||
"FORM_ERROR_MESSAGE": "Please fill all variables before sending",
|
||||
"MEDIA_HEADER_LABEL": "{type} Header",
|
||||
"OTP_CODE": "Enter 4-8 digit OTP",
|
||||
"EXPIRY_MINUTES": "Enter expiry minutes",
|
||||
"BUTTON_PARAMETERS": "Button Parameters",
|
||||
"BUTTON_LABEL": "Button {index}",
|
||||
"COUPON_CODE": "Enter coupon code (max 15 chars)",
|
||||
"MEDIA_URL_LABEL": "Enter {type} URL",
|
||||
"BUTTON_PARAMETER": "Enter button parameter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
"CONVERSATION_CREATED": "Conversation Created",
|
||||
"CONVERSATION_UPDATED": "Conversation Updated",
|
||||
"MESSAGE_CREATED": "Message Created",
|
||||
"CONVERSATION_RESOLVED": "Conversation Resolved",
|
||||
"CONVERSATION_OPENED": "Conversation Opened"
|
||||
},
|
||||
"ACTIONS": {
|
||||
|
||||
@@ -472,7 +472,8 @@
|
||||
"FEATURES": {
|
||||
"TITLE": "Features",
|
||||
"ALLOW_CONVERSATION_FAQS": "Generate FAQs from resolved conversations",
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions."
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions.",
|
||||
"ALLOW_CITATIONS": "Include source citations in responses"
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
|
||||
@@ -1,29 +1,46 @@
|
||||
{
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "Whatsapp Templates",
|
||||
"SUBTITLE": "Select the whatsapp template you want to send",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Process {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "Search Templates",
|
||||
"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": {
|
||||
"LANGUAGE": "Language",
|
||||
"TEMPLATE_BODY": "Template Body",
|
||||
"CATEGORY": "Category"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "Variables",
|
||||
"VARIABLE_PLACEHOLDER": "Enter {variable} value",
|
||||
"GO_BACK_LABEL": "Go Back",
|
||||
"SEND_MESSAGE_LABEL": "Send Message",
|
||||
"FORM_ERROR_MESSAGE": "Please fill all variables before sending"
|
||||
}
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "Whatsapp Templates",
|
||||
"SUBTITLE": "Select the whatsapp template you want to send",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Configure template: {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "Search Templates",
|
||||
"NO_TEMPLATES_FOUND": "No templates found for",
|
||||
"HEADER": "Header",
|
||||
"BODY": "Body",
|
||||
"FOOTER": "Footer",
|
||||
"BUTTONS": "Buttons",
|
||||
"CATEGORY": "Category",
|
||||
"MEDIA_CONTENT": "Media Content",
|
||||
"MEDIA_CONTENT_FALLBACK": "media content",
|
||||
"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": {
|
||||
"LANGUAGE": "Language",
|
||||
"TEMPLATE_BODY": "Template Body",
|
||||
"CATEGORY": "Category"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "Variables",
|
||||
"LANGUAGE": "Language",
|
||||
"CATEGORY": "Category",
|
||||
"VARIABLE_PLACEHOLDER": "Enter {variable} value",
|
||||
"GO_BACK_LABEL": "Go Back",
|
||||
"SEND_MESSAGE_LABEL": "Send Message",
|
||||
"FORM_ERROR_MESSAGE": "Please fill all variables before sending",
|
||||
"MEDIA_HEADER_LABEL": "{type} Header",
|
||||
"OTP_CODE": "Enter 4-8 digit OTP",
|
||||
"EXPIRY_MINUTES": "Enter expiry minutes",
|
||||
"BUTTON_PARAMETERS": "Button Parameters",
|
||||
"BUTTON_LABEL": "Button {index}",
|
||||
"COUPON_CODE": "Enter coupon code (max 15 chars)",
|
||||
"MEDIA_URL_LABEL": "Enter {type} URL",
|
||||
"BUTTON_PARAMETER": "Enter button parameter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
"CONVERSATION_CREATED": "Conversa Creada",
|
||||
"CONVERSATION_UPDATED": "Conversa Actualitzada",
|
||||
"MESSAGE_CREATED": "Message Created",
|
||||
"CONVERSATION_RESOLVED": "Conversation Resolved",
|
||||
"CONVERSATION_OPENED": "Conversation Opened"
|
||||
},
|
||||
"ACTIONS": {
|
||||
|
||||
@@ -472,7 +472,8 @@
|
||||
"FEATURES": {
|
||||
"TITLE": "Característiques",
|
||||
"ALLOW_CONVERSATION_FAQS": "Generate FAQs from resolved conversations",
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions."
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions.",
|
||||
"ALLOW_CITATIONS": "Include source citations in responses"
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
|
||||
@@ -1,29 +1,46 @@
|
||||
{
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "Plantilles de Whatsapp",
|
||||
"SUBTITLE": "Selecciona la plantilla de whatsapp que vols enviar",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Procés {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "Cerca plantilles",
|
||||
"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": {
|
||||
"LANGUAGE": "Idioma",
|
||||
"TEMPLATE_BODY": "Cos de la plantilla",
|
||||
"CATEGORY": "Categoria"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "Variables",
|
||||
"VARIABLE_PLACEHOLDER": "Introdueix el valor {variable}",
|
||||
"GO_BACK_LABEL": "Torna enrere",
|
||||
"SEND_MESSAGE_LABEL": "Envia missatge",
|
||||
"FORM_ERROR_MESSAGE": "Omple totes les variables abans d'enviar-les"
|
||||
}
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "Plantilles de Whatsapp",
|
||||
"SUBTITLE": "Selecciona la plantilla de whatsapp que vols enviar",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Configure template: {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "Cerca plantilles",
|
||||
"NO_TEMPLATES_FOUND": "No s'han trobat plantilles per a",
|
||||
"HEADER": "Header",
|
||||
"BODY": "Body",
|
||||
"FOOTER": "Footer",
|
||||
"BUTTONS": "Buttons",
|
||||
"CATEGORY": "Categoria",
|
||||
"MEDIA_CONTENT": "Media Content",
|
||||
"MEDIA_CONTENT_FALLBACK": "media content",
|
||||
"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": {
|
||||
"LANGUAGE": "Idioma",
|
||||
"TEMPLATE_BODY": "Cos de la plantilla",
|
||||
"CATEGORY": "Categoria"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "Variables",
|
||||
"LANGUAGE": "Idioma",
|
||||
"CATEGORY": "Categoria",
|
||||
"VARIABLE_PLACEHOLDER": "Introdueix el valor {variable}",
|
||||
"GO_BACK_LABEL": "Torna enrere",
|
||||
"SEND_MESSAGE_LABEL": "Envia missatge",
|
||||
"FORM_ERROR_MESSAGE": "Omple totes les variables abans d'enviar-les",
|
||||
"MEDIA_HEADER_LABEL": "{type} Header",
|
||||
"OTP_CODE": "Enter 4-8 digit OTP",
|
||||
"EXPIRY_MINUTES": "Enter expiry minutes",
|
||||
"BUTTON_PARAMETERS": "Button Parameters",
|
||||
"BUTTON_LABEL": "Button {index}",
|
||||
"COUPON_CODE": "Enter coupon code (max 15 chars)",
|
||||
"MEDIA_URL_LABEL": "Enter {type} URL",
|
||||
"BUTTON_PARAMETER": "Enter button parameter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
"CONVERSATION_CREATED": "Conversation Created",
|
||||
"CONVERSATION_UPDATED": "Conversation Updated",
|
||||
"MESSAGE_CREATED": "Zpráva vytvořena",
|
||||
"CONVERSATION_RESOLVED": "Conversation Resolved",
|
||||
"CONVERSATION_OPENED": "Konverzace otevřena"
|
||||
},
|
||||
"ACTIONS": {
|
||||
|
||||
@@ -472,7 +472,8 @@
|
||||
"FEATURES": {
|
||||
"TITLE": "Funkce",
|
||||
"ALLOW_CONVERSATION_FAQS": "Generate FAQs from resolved conversations",
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions."
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions.",
|
||||
"ALLOW_CITATIONS": "Include source citations in responses"
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
|
||||
@@ -1,29 +1,46 @@
|
||||
{
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "Whatsapp Templates",
|
||||
"SUBTITLE": "Select the whatsapp template you want to send",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Process {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "Search Templates",
|
||||
"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": {
|
||||
"LANGUAGE": "Language",
|
||||
"TEMPLATE_BODY": "Template Body",
|
||||
"CATEGORY": "Category"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "Variables",
|
||||
"VARIABLE_PLACEHOLDER": "Enter {variable} value",
|
||||
"GO_BACK_LABEL": "Go Back",
|
||||
"SEND_MESSAGE_LABEL": "Send Message",
|
||||
"FORM_ERROR_MESSAGE": "Please fill all variables before sending"
|
||||
}
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "Whatsapp Templates",
|
||||
"SUBTITLE": "Select the whatsapp template you want to send",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Configure template: {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "Search Templates",
|
||||
"NO_TEMPLATES_FOUND": "No templates found for",
|
||||
"HEADER": "Header",
|
||||
"BODY": "Body",
|
||||
"FOOTER": "Footer",
|
||||
"BUTTONS": "Buttons",
|
||||
"CATEGORY": "Category",
|
||||
"MEDIA_CONTENT": "Media Content",
|
||||
"MEDIA_CONTENT_FALLBACK": "media content",
|
||||
"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": {
|
||||
"LANGUAGE": "Language",
|
||||
"TEMPLATE_BODY": "Template Body",
|
||||
"CATEGORY": "Category"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "Variables",
|
||||
"LANGUAGE": "Language",
|
||||
"CATEGORY": "Category",
|
||||
"VARIABLE_PLACEHOLDER": "Enter {variable} value",
|
||||
"GO_BACK_LABEL": "Go Back",
|
||||
"SEND_MESSAGE_LABEL": "Send Message",
|
||||
"FORM_ERROR_MESSAGE": "Please fill all variables before sending",
|
||||
"MEDIA_HEADER_LABEL": "{type} Header",
|
||||
"OTP_CODE": "Enter 4-8 digit OTP",
|
||||
"EXPIRY_MINUTES": "Enter expiry minutes",
|
||||
"BUTTON_PARAMETERS": "Button Parameters",
|
||||
"BUTTON_LABEL": "Button {index}",
|
||||
"COUPON_CODE": "Enter coupon code (max 15 chars)",
|
||||
"MEDIA_URL_LABEL": "Enter {type} URL",
|
||||
"BUTTON_PARAMETER": "Enter button parameter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
"CONVERSATION_CREATED": "Samtale Oprettet",
|
||||
"CONVERSATION_UPDATED": "Samtale Opdateret",
|
||||
"MESSAGE_CREATED": "Message Created",
|
||||
"CONVERSATION_RESOLVED": "Conversation Resolved",
|
||||
"CONVERSATION_OPENED": "Conversation Opened"
|
||||
},
|
||||
"ACTIONS": {
|
||||
|
||||
@@ -472,7 +472,8 @@
|
||||
"FEATURES": {
|
||||
"TITLE": "Funktioner",
|
||||
"ALLOW_CONVERSATION_FAQS": "Generate FAQs from resolved conversations",
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions."
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions.",
|
||||
"ALLOW_CITATIONS": "Include source citations in responses"
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
|
||||
@@ -1,29 +1,46 @@
|
||||
{
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "Whatsapp Skabeloner",
|
||||
"SUBTITLE": "Vælg den whatsapp skabelon, du vil sende",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Proces {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "Søg Skabeloner",
|
||||
"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": {
|
||||
"LANGUAGE": "Sprog",
|
||||
"TEMPLATE_BODY": "Skabelon Krop",
|
||||
"CATEGORY": "Kategori"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "Variabler",
|
||||
"VARIABLE_PLACEHOLDER": "Indtast {variable} værdi",
|
||||
"GO_BACK_LABEL": "Gå Tilbage",
|
||||
"SEND_MESSAGE_LABEL": "Send Besked",
|
||||
"FORM_ERROR_MESSAGE": "Udfyld venligst alle variabler før afsendelse"
|
||||
}
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "Whatsapp Skabeloner",
|
||||
"SUBTITLE": "Vælg den whatsapp skabelon, du vil sende",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Configure template: {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "Søg Skabeloner",
|
||||
"NO_TEMPLATES_FOUND": "Ingen skabeloner fundet for",
|
||||
"HEADER": "Header",
|
||||
"BODY": "Body",
|
||||
"FOOTER": "Footer",
|
||||
"BUTTONS": "Buttons",
|
||||
"CATEGORY": "Kategori",
|
||||
"MEDIA_CONTENT": "Media Content",
|
||||
"MEDIA_CONTENT_FALLBACK": "media content",
|
||||
"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": {
|
||||
"LANGUAGE": "Sprog",
|
||||
"TEMPLATE_BODY": "Skabelon Krop",
|
||||
"CATEGORY": "Kategori"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "Variabler",
|
||||
"LANGUAGE": "Sprog",
|
||||
"CATEGORY": "Kategori",
|
||||
"VARIABLE_PLACEHOLDER": "Indtast {variable} værdi",
|
||||
"GO_BACK_LABEL": "Gå Tilbage",
|
||||
"SEND_MESSAGE_LABEL": "Send Besked",
|
||||
"FORM_ERROR_MESSAGE": "Udfyld venligst alle variabler før afsendelse",
|
||||
"MEDIA_HEADER_LABEL": "{type} Header",
|
||||
"OTP_CODE": "Enter 4-8 digit OTP",
|
||||
"EXPIRY_MINUTES": "Enter expiry minutes",
|
||||
"BUTTON_PARAMETERS": "Button Parameters",
|
||||
"BUTTON_LABEL": "Button {index}",
|
||||
"COUPON_CODE": "Enter coupon code (max 15 chars)",
|
||||
"MEDIA_URL_LABEL": "Enter {type} URL",
|
||||
"BUTTON_PARAMETER": "Enter button parameter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
"CONVERSATION_CREATED": "Konversation erstellt",
|
||||
"CONVERSATION_UPDATED": "Konversation aktualisiert",
|
||||
"MESSAGE_CREATED": "Message Created",
|
||||
"CONVERSATION_RESOLVED": "Conversation Resolved",
|
||||
"CONVERSATION_OPENED": "Conversation Opened"
|
||||
},
|
||||
"ACTIONS": {
|
||||
|
||||
@@ -472,7 +472,8 @@
|
||||
"FEATURES": {
|
||||
"TITLE": "Funktionen",
|
||||
"ALLOW_CONVERSATION_FAQS": "Generate FAQs from resolved conversations",
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions."
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions.",
|
||||
"ALLOW_CITATIONS": "Include source citations in responses"
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
|
||||
@@ -1,29 +1,46 @@
|
||||
{
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "WhatsApp-Vorlagen",
|
||||
"SUBTITLE": "Wählen Sie die WhatsApp-Vorlage aus, die Sie senden möchten",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Verarbeite {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "Vorlagen suchen",
|
||||
"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": {
|
||||
"LANGUAGE": "Sprache",
|
||||
"TEMPLATE_BODY": "Vorlagenbody",
|
||||
"CATEGORY": "Kategorie"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "Variablen",
|
||||
"VARIABLE_PLACEHOLDER": "Geben Sie den Wert {variable} ein",
|
||||
"GO_BACK_LABEL": "Zurück",
|
||||
"SEND_MESSAGE_LABEL": "Nachricht senden",
|
||||
"FORM_ERROR_MESSAGE": "Bitte füllen Sie vor dem Absenden alle Variablen aus"
|
||||
}
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "WhatsApp-Vorlagen",
|
||||
"SUBTITLE": "Wählen Sie die WhatsApp-Vorlage aus, die Sie senden möchten",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Configure template: {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "Vorlagen suchen",
|
||||
"NO_TEMPLATES_FOUND": "Keine Vorlagen gefunden für",
|
||||
"HEADER": "Header",
|
||||
"BODY": "Body",
|
||||
"FOOTER": "Footer",
|
||||
"BUTTONS": "Buttons",
|
||||
"CATEGORY": "Kategorie",
|
||||
"MEDIA_CONTENT": "Media Content",
|
||||
"MEDIA_CONTENT_FALLBACK": "media content",
|
||||
"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": {
|
||||
"LANGUAGE": "Sprache",
|
||||
"TEMPLATE_BODY": "Vorlagenbody",
|
||||
"CATEGORY": "Kategorie"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "Variablen",
|
||||
"LANGUAGE": "Sprache",
|
||||
"CATEGORY": "Kategorie",
|
||||
"VARIABLE_PLACEHOLDER": "Geben Sie den Wert {variable} ein",
|
||||
"GO_BACK_LABEL": "Zurück",
|
||||
"SEND_MESSAGE_LABEL": "Nachricht senden",
|
||||
"FORM_ERROR_MESSAGE": "Bitte füllen Sie vor dem Absenden alle Variablen aus",
|
||||
"MEDIA_HEADER_LABEL": "{type} Header",
|
||||
"OTP_CODE": "Enter 4-8 digit OTP",
|
||||
"EXPIRY_MINUTES": "Enter expiry minutes",
|
||||
"BUTTON_PARAMETERS": "Button Parameters",
|
||||
"BUTTON_LABEL": "Button {index}",
|
||||
"COUPON_CODE": "Enter coupon code (max 15 chars)",
|
||||
"MEDIA_URL_LABEL": "Enter {type} URL",
|
||||
"BUTTON_PARAMETER": "Enter button parameter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
"CONVERSATION_CREATED": "Δημιουργήθηκε Συνομιλία",
|
||||
"CONVERSATION_UPDATED": "Η Συνομιλία Ενημερώθηκε",
|
||||
"MESSAGE_CREATED": "Message Created",
|
||||
"CONVERSATION_RESOLVED": "Conversation Resolved",
|
||||
"CONVERSATION_OPENED": "Conversation Opened"
|
||||
},
|
||||
"ACTIONS": {
|
||||
|
||||
@@ -472,7 +472,8 @@
|
||||
"FEATURES": {
|
||||
"TITLE": "Χαρακτηριστικά",
|
||||
"ALLOW_CONVERSATION_FAQS": "Generate FAQs from resolved conversations",
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions."
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions.",
|
||||
"ALLOW_CITATIONS": "Include source citations in responses"
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
|
||||
@@ -1,29 +1,46 @@
|
||||
{
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "Πρότυπα Whatsapp",
|
||||
"SUBTITLE": "Επιλέξτε το πρότυπο Whatsapp που θέλετε να στείλετε",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Επεξεργασία {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "Αναζήτηση Προτύπων",
|
||||
"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": {
|
||||
"LANGUAGE": "Γλώσσα",
|
||||
"TEMPLATE_BODY": "Σώμα Προτύπου",
|
||||
"CATEGORY": "Κατηγορία"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "Μεταβλητές",
|
||||
"VARIABLE_PLACEHOLDER": "Εισάγετε τιμή για {variable}",
|
||||
"GO_BACK_LABEL": "Πίσω",
|
||||
"SEND_MESSAGE_LABEL": "Αποστολή μηνύματος",
|
||||
"FORM_ERROR_MESSAGE": "Παρακαλώ συμπληρώστε όλες τις μεταβλητές πριν την αποστολή"
|
||||
}
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "Πρότυπα Whatsapp",
|
||||
"SUBTITLE": "Επιλέξτε το πρότυπο Whatsapp που θέλετε να στείλετε",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Configure template: {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "Αναζήτηση Προτύπων",
|
||||
"NO_TEMPLATES_FOUND": "Δεν βρέθηκαν πρότυπα για",
|
||||
"HEADER": "Header",
|
||||
"BODY": "Body",
|
||||
"FOOTER": "Footer",
|
||||
"BUTTONS": "Buttons",
|
||||
"CATEGORY": "Κατηγορία",
|
||||
"MEDIA_CONTENT": "Media Content",
|
||||
"MEDIA_CONTENT_FALLBACK": "media content",
|
||||
"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": {
|
||||
"LANGUAGE": "Γλώσσα",
|
||||
"TEMPLATE_BODY": "Σώμα Προτύπου",
|
||||
"CATEGORY": "Κατηγορία"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "Μεταβλητές",
|
||||
"LANGUAGE": "Γλώσσα",
|
||||
"CATEGORY": "Κατηγορία",
|
||||
"VARIABLE_PLACEHOLDER": "Εισάγετε τιμή για {variable}",
|
||||
"GO_BACK_LABEL": "Πίσω",
|
||||
"SEND_MESSAGE_LABEL": "Αποστολή μηνύματος",
|
||||
"FORM_ERROR_MESSAGE": "Παρακαλώ συμπληρώστε όλες τις μεταβλητές πριν την αποστολή",
|
||||
"MEDIA_HEADER_LABEL": "{type} Header",
|
||||
"OTP_CODE": "Enter 4-8 digit OTP",
|
||||
"EXPIRY_MINUTES": "Enter expiry minutes",
|
||||
"BUTTON_PARAMETERS": "Button Parameters",
|
||||
"BUTTON_LABEL": "Button {index}",
|
||||
"COUPON_CODE": "Enter coupon code (max 15 chars)",
|
||||
"MEDIA_URL_LABEL": "Enter {type} URL",
|
||||
"BUTTON_PARAMETER": "Enter button parameter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
"CONVERSATION_CREATED": "Conversation Created",
|
||||
"CONVERSATION_UPDATED": "Conversation Updated",
|
||||
"MESSAGE_CREATED": "Message Created",
|
||||
"CONVERSATION_RESOLVED": "Conversation Resolved",
|
||||
"CONVERSATION_OPENED": "Conversation Opened"
|
||||
},
|
||||
"ACTIONS": {
|
||||
|
||||
@@ -17,6 +17,11 @@
|
||||
"IP_ADDRESS": "IP Address",
|
||||
"CREATED_AT_LABEL": "Created",
|
||||
"NEW_MESSAGE": "New message",
|
||||
"CALL": "Call",
|
||||
"CALL_UNDER_DEVELOPMENT": "Calling is under development",
|
||||
"VOICE_INBOX_PICKER": {
|
||||
"TITLE": "Choose a voice inbox"
|
||||
},
|
||||
"CONVERSATIONS": {
|
||||
"NO_RECORDS_FOUND": "There are no previous conversations associated to this contact.",
|
||||
"TITLE": "Previous Conversations"
|
||||
|
||||
@@ -272,8 +272,8 @@
|
||||
},
|
||||
"SUBMIT_BUTTON": "Create WhatsApp Channel",
|
||||
"EMBEDDED_SIGNUP": {
|
||||
"TITLE": "Quick Setup with Meta",
|
||||
"DESC": "You will be redirected to Meta to log into your WhatsApp Business account. Having admin access will help make the setup smooth and easy.",
|
||||
"TITLE": "Quick setup with Meta",
|
||||
"DESC": "Use the WhatsApp Embedded Signup flow to quickly connect new numbers. You will be redirected to Meta to log into your WhatsApp Business account. Having admin access will help make the setup smooth and easy.",
|
||||
"BENEFITS": {
|
||||
"TITLE": "Benefits of Embedded Signup:",
|
||||
"EASY_SETUP": "No manual configuration required",
|
||||
@@ -296,7 +296,10 @@
|
||||
"INVALID_BUSINESS_DATA": "Invalid business data received from Facebook. Please try again.",
|
||||
"SIGNUP_ERROR": "Signup error occurred",
|
||||
"AUTH_NOT_COMPLETED": "Authentication not completed. Please restart the process.",
|
||||
"SUCCESS_FALLBACK": "WhatsApp Business Account has been successfully configured"
|
||||
"SUCCESS_FALLBACK": "WhatsApp Business Account has been successfully configured",
|
||||
"MANUAL_FALLBACK": "If your number is already linked to an Embedded Signup, or if you're a tech provider onboarding your own number, please use the",
|
||||
"MANUAL_SETUP_LINK": "manual setup flow",
|
||||
"MANUAL_FALLBACK_INSTEAD": "instead"
|
||||
},
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "We were not able to save the WhatsApp channel"
|
||||
@@ -330,13 +333,14 @@
|
||||
"LABEL": "API Key Secret",
|
||||
"PLACEHOLDER": "Enter your Twilio API Key Secret",
|
||||
"REQUIRED": "API Key Secret is required"
|
||||
},
|
||||
"TWIML_APP_SID": {
|
||||
"LABEL": "TwiML App SID",
|
||||
"PLACEHOLDER": "Enter your Twilio TwiML App SID (starts with AP)",
|
||||
"REQUIRED": "TwiML App SID is required"
|
||||
}
|
||||
},
|
||||
"CONFIGURATION": {
|
||||
"TWILIO_VOICE_URL_TITLE": "Twilio Voice URL",
|
||||
"TWILIO_VOICE_URL_SUBTITLE": "Configure this URL as the Voice URL on your Twilio phone number and TwiML App.",
|
||||
"TWILIO_STATUS_URL_TITLE": "Twilio Status Callback URL",
|
||||
"TWILIO_STATUS_URL_SUBTITLE": "Configure this URL as the Status Callback URL on your Twilio phone number."
|
||||
},
|
||||
"SUBMIT_BUTTON": "Create Voice Channel",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "We were not able to create the voice channel"
|
||||
|
||||
@@ -226,6 +226,7 @@
|
||||
"APPEARANCE": "Change appearance",
|
||||
"SUPER_ADMIN_CONSOLE": "SuperAdmin console",
|
||||
"DOCS": "Read documentation",
|
||||
"CHANGELOG": "Changelog",
|
||||
"LOGOUT": "Log out"
|
||||
},
|
||||
"APP_GLOBAL": {
|
||||
|
||||
@@ -1,29 +1,46 @@
|
||||
{
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "Whatsapp Templates",
|
||||
"SUBTITLE": "Select the whatsapp template you want to send",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Process {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "Search Templates",
|
||||
"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": {
|
||||
"LANGUAGE": "Language",
|
||||
"TEMPLATE_BODY": "Template Body",
|
||||
"CATEGORY": "Category"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "Variables",
|
||||
"VARIABLE_PLACEHOLDER": "Enter {variable} value",
|
||||
"GO_BACK_LABEL": "Go Back",
|
||||
"SEND_MESSAGE_LABEL": "Send Message",
|
||||
"FORM_ERROR_MESSAGE": "Please fill all variables before sending"
|
||||
}
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "Whatsapp Templates",
|
||||
"SUBTITLE": "Select the whatsapp template you want to send",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Configure template: {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "Search Templates",
|
||||
"NO_TEMPLATES_FOUND": "No templates found for",
|
||||
"HEADER": "Header",
|
||||
"BODY": "Body",
|
||||
"FOOTER": "Footer",
|
||||
"BUTTONS": "Buttons",
|
||||
"CATEGORY": "Category",
|
||||
"MEDIA_CONTENT": "Media Content",
|
||||
"MEDIA_CONTENT_FALLBACK": "media content",
|
||||
"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": {
|
||||
"LANGUAGE": "Language",
|
||||
"TEMPLATE_BODY": "Template Body",
|
||||
"CATEGORY": "Category"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "Variables",
|
||||
"LANGUAGE": "Language",
|
||||
"CATEGORY": "Category",
|
||||
"VARIABLE_PLACEHOLDER": "Enter {variable} value",
|
||||
"GO_BACK_LABEL": "Go Back",
|
||||
"SEND_MESSAGE_LABEL": "Send Message",
|
||||
"FORM_ERROR_MESSAGE": "Please fill all variables before sending",
|
||||
"MEDIA_HEADER_LABEL": "{type} Header",
|
||||
"OTP_CODE": "Enter 4-8 digit OTP",
|
||||
"EXPIRY_MINUTES": "Enter expiry minutes",
|
||||
"BUTTON_PARAMETERS": "Button Parameters",
|
||||
"BUTTON_LABEL": "Button {index}",
|
||||
"COUPON_CODE": "Enter coupon code (max 15 chars)",
|
||||
"MEDIA_URL_LABEL": "Enter {type} URL",
|
||||
"BUTTON_PARAMETER": "Enter button parameter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
"CONVERSATION_CREATED": "Conversación creada",
|
||||
"CONVERSATION_UPDATED": "Conversación actualizada",
|
||||
"MESSAGE_CREATED": "Mensaje creado",
|
||||
"CONVERSATION_RESOLVED": "Conversation Resolved",
|
||||
"CONVERSATION_OPENED": "Conversación abierta"
|
||||
},
|
||||
"ACTIONS": {
|
||||
|
||||
@@ -472,7 +472,8 @@
|
||||
"FEATURES": {
|
||||
"TITLE": "Características",
|
||||
"ALLOW_CONVERSATION_FAQS": "Generate FAQs from resolved conversations",
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions."
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions.",
|
||||
"ALLOW_CITATIONS": "Include source citations in responses"
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
|
||||
@@ -1,29 +1,46 @@
|
||||
{
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "Plantillas de Whatsapp",
|
||||
"SUBTITLE": "Seleccione la plantilla de Whatsapp que desea enviar",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Procesar {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "Buscar plantillas",
|
||||
"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": {
|
||||
"LANGUAGE": "Idioma",
|
||||
"TEMPLATE_BODY": "Cuerpo de plantilla",
|
||||
"CATEGORY": "Categoría"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "Variables",
|
||||
"VARIABLE_PLACEHOLDER": "Introduzca el valor de {variable}",
|
||||
"GO_BACK_LABEL": "Volver",
|
||||
"SEND_MESSAGE_LABEL": "Enviar mensaje",
|
||||
"FORM_ERROR_MESSAGE": "Por favor, rellene todas las variables antes de enviar"
|
||||
}
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "Plantillas de Whatsapp",
|
||||
"SUBTITLE": "Seleccione la plantilla de Whatsapp que desea enviar",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Configure template: {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "Buscar plantillas",
|
||||
"NO_TEMPLATES_FOUND": "No se encontraron plantillas para",
|
||||
"HEADER": "Header",
|
||||
"BODY": "Body",
|
||||
"FOOTER": "Footer",
|
||||
"BUTTONS": "Buttons",
|
||||
"CATEGORY": "Categoría",
|
||||
"MEDIA_CONTENT": "Media Content",
|
||||
"MEDIA_CONTENT_FALLBACK": "media content",
|
||||
"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": {
|
||||
"LANGUAGE": "Idioma",
|
||||
"TEMPLATE_BODY": "Cuerpo de plantilla",
|
||||
"CATEGORY": "Categoría"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "Variables",
|
||||
"LANGUAGE": "Idioma",
|
||||
"CATEGORY": "Categoría",
|
||||
"VARIABLE_PLACEHOLDER": "Introduzca el valor de {variable}",
|
||||
"GO_BACK_LABEL": "Volver",
|
||||
"SEND_MESSAGE_LABEL": "Enviar mensaje",
|
||||
"FORM_ERROR_MESSAGE": "Por favor, rellene todas las variables antes de enviar",
|
||||
"MEDIA_HEADER_LABEL": "{type} Header",
|
||||
"OTP_CODE": "Enter 4-8 digit OTP",
|
||||
"EXPIRY_MINUTES": "Enter expiry minutes",
|
||||
"BUTTON_PARAMETERS": "Button Parameters",
|
||||
"BUTTON_LABEL": "Button {index}",
|
||||
"COUPON_CODE": "Enter coupon code (max 15 chars)",
|
||||
"MEDIA_URL_LABEL": "Enter {type} URL",
|
||||
"BUTTON_PARAMETER": "Enter button parameter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
"CONVERSATION_CREATED": "گفتگو ایجاد شد",
|
||||
"CONVERSATION_UPDATED": "گفتگو به روز شد",
|
||||
"MESSAGE_CREATED": "Message Created",
|
||||
"CONVERSATION_RESOLVED": "Conversation Resolved",
|
||||
"CONVERSATION_OPENED": "Conversation Opened"
|
||||
},
|
||||
"ACTIONS": {
|
||||
|
||||
@@ -472,7 +472,8 @@
|
||||
"FEATURES": {
|
||||
"TITLE": "امکانات",
|
||||
"ALLOW_CONVERSATION_FAQS": "Generate FAQs from resolved conversations",
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions."
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions.",
|
||||
"ALLOW_CITATIONS": "Include source citations in responses"
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
|
||||
@@ -1,29 +1,46 @@
|
||||
{
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "قالب های واتساپ",
|
||||
"SUBTITLE": "قالب واتساپ مورد نظر برای ارسال را انتخاب کنید",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "فرآیند {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "جستجوی الگوها",
|
||||
"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": {
|
||||
"LANGUAGE": "زبان",
|
||||
"TEMPLATE_BODY": "بدنه الگو",
|
||||
"CATEGORY": "دستهبندی"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "متغیرها",
|
||||
"VARIABLE_PLACEHOLDER": "مقدار {variable} را وارد کنید",
|
||||
"GO_BACK_LABEL": "بازگشت",
|
||||
"SEND_MESSAGE_LABEL": "ارسال پیام",
|
||||
"FORM_ERROR_MESSAGE": "لطفا قبل از ارسال همه متغیرها را پر کنید"
|
||||
}
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "قالب های واتساپ",
|
||||
"SUBTITLE": "قالب واتساپ مورد نظر برای ارسال را انتخاب کنید",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Configure template: {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "جستجوی الگوها",
|
||||
"NO_TEMPLATES_FOUND": "هیچ قالبی برای",
|
||||
"HEADER": "Header",
|
||||
"BODY": "Body",
|
||||
"FOOTER": "Footer",
|
||||
"BUTTONS": "Buttons",
|
||||
"CATEGORY": "دستهبندی",
|
||||
"MEDIA_CONTENT": "Media Content",
|
||||
"MEDIA_CONTENT_FALLBACK": "media content",
|
||||
"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": {
|
||||
"LANGUAGE": "زبان",
|
||||
"TEMPLATE_BODY": "بدنه الگو",
|
||||
"CATEGORY": "دستهبندی"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "متغیرها",
|
||||
"LANGUAGE": "زبان",
|
||||
"CATEGORY": "دستهبندی",
|
||||
"VARIABLE_PLACEHOLDER": "مقدار {variable} را وارد کنید",
|
||||
"GO_BACK_LABEL": "بازگشت",
|
||||
"SEND_MESSAGE_LABEL": "ارسال پیام",
|
||||
"FORM_ERROR_MESSAGE": "لطفا قبل از ارسال همه متغیرها را پر کنید",
|
||||
"MEDIA_HEADER_LABEL": "{type} Header",
|
||||
"OTP_CODE": "Enter 4-8 digit OTP",
|
||||
"EXPIRY_MINUTES": "Enter expiry minutes",
|
||||
"BUTTON_PARAMETERS": "Button Parameters",
|
||||
"BUTTON_LABEL": "Button {index}",
|
||||
"COUPON_CODE": "Enter coupon code (max 15 chars)",
|
||||
"MEDIA_URL_LABEL": "Enter {type} URL",
|
||||
"BUTTON_PARAMETER": "Enter button parameter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
"CONVERSATION_CREATED": "Conversation Created",
|
||||
"CONVERSATION_UPDATED": "Conversation Updated",
|
||||
"MESSAGE_CREATED": "Message Created",
|
||||
"CONVERSATION_RESOLVED": "Conversation Resolved",
|
||||
"CONVERSATION_OPENED": "Conversation Opened"
|
||||
},
|
||||
"ACTIONS": {
|
||||
|
||||
@@ -472,7 +472,8 @@
|
||||
"FEATURES": {
|
||||
"TITLE": "Ominaisuudet",
|
||||
"ALLOW_CONVERSATION_FAQS": "Generate FAQs from resolved conversations",
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions."
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions.",
|
||||
"ALLOW_CITATIONS": "Include source citations in responses"
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
|
||||
@@ -1,29 +1,46 @@
|
||||
{
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "WhatsApp-pohjat",
|
||||
"SUBTITLE": "Valitse WhatsApp-pohja, jonka haluat lähettää",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Process {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "Etsi Pohjia",
|
||||
"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": {
|
||||
"LANGUAGE": "Language",
|
||||
"TEMPLATE_BODY": "Template Body",
|
||||
"CATEGORY": "Category"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "Muuttujat",
|
||||
"VARIABLE_PLACEHOLDER": "Enter {variable} value",
|
||||
"GO_BACK_LABEL": "Mene Takaisin",
|
||||
"SEND_MESSAGE_LABEL": "Lähetä Viesti",
|
||||
"FORM_ERROR_MESSAGE": "Täytä kaikki muuttujat ennen lähettämistä"
|
||||
}
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "WhatsApp-pohjat",
|
||||
"SUBTITLE": "Valitse WhatsApp-pohja, jonka haluat lähettää",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Configure template: {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "Etsi Pohjia",
|
||||
"NO_TEMPLATES_FOUND": "No templates found for",
|
||||
"HEADER": "Header",
|
||||
"BODY": "Body",
|
||||
"FOOTER": "Footer",
|
||||
"BUTTONS": "Buttons",
|
||||
"CATEGORY": "Category",
|
||||
"MEDIA_CONTENT": "Media Content",
|
||||
"MEDIA_CONTENT_FALLBACK": "media content",
|
||||
"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": {
|
||||
"LANGUAGE": "Language",
|
||||
"TEMPLATE_BODY": "Template Body",
|
||||
"CATEGORY": "Category"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "Muuttujat",
|
||||
"LANGUAGE": "Language",
|
||||
"CATEGORY": "Category",
|
||||
"VARIABLE_PLACEHOLDER": "Enter {variable} value",
|
||||
"GO_BACK_LABEL": "Mene Takaisin",
|
||||
"SEND_MESSAGE_LABEL": "Lähetä Viesti",
|
||||
"FORM_ERROR_MESSAGE": "Täytä kaikki muuttujat ennen lähettämistä",
|
||||
"MEDIA_HEADER_LABEL": "{type} Header",
|
||||
"OTP_CODE": "Enter 4-8 digit OTP",
|
||||
"EXPIRY_MINUTES": "Enter expiry minutes",
|
||||
"BUTTON_PARAMETERS": "Button Parameters",
|
||||
"BUTTON_LABEL": "Button {index}",
|
||||
"COUPON_CODE": "Enter coupon code (max 15 chars)",
|
||||
"MEDIA_URL_LABEL": "Enter {type} URL",
|
||||
"BUTTON_PARAMETER": "Enter button parameter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
"CONVERSATION_CREATED": "Conversation créée",
|
||||
"CONVERSATION_UPDATED": "Conversation mise à jour",
|
||||
"MESSAGE_CREATED": "Message créé",
|
||||
"CONVERSATION_RESOLVED": "Conversation Resolved",
|
||||
"CONVERSATION_OPENED": "Conversation ouverte"
|
||||
},
|
||||
"ACTIONS": {
|
||||
|
||||
@@ -472,7 +472,8 @@
|
||||
"FEATURES": {
|
||||
"TITLE": "Fonctionnalités",
|
||||
"ALLOW_CONVERSATION_FAQS": "Generate FAQs from resolved conversations",
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions."
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions.",
|
||||
"ALLOW_CITATIONS": "Include source citations in responses"
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
|
||||
@@ -1,29 +1,46 @@
|
||||
{
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "Modèles WhatsApp",
|
||||
"SUBTITLE": "Sélectionnez le modèle whatsapp que vous souhaitez envoyer",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Traiter {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "Rechercher des modèles",
|
||||
"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": {
|
||||
"LANGUAGE": "Langue",
|
||||
"TEMPLATE_BODY": "Corps du modèle",
|
||||
"CATEGORY": "Catégorie"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "Variables",
|
||||
"VARIABLE_PLACEHOLDER": "Entrez la valeur de {variable}",
|
||||
"GO_BACK_LABEL": "Retour",
|
||||
"SEND_MESSAGE_LABEL": "Envoyer un message",
|
||||
"FORM_ERROR_MESSAGE": "Veuillez remplir toutes les variables avant d'envoyer"
|
||||
}
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "Modèles WhatsApp",
|
||||
"SUBTITLE": "Sélectionnez le modèle whatsapp que vous souhaitez envoyer",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Configure template: {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "Rechercher des modèles",
|
||||
"NO_TEMPLATES_FOUND": "Aucun modèle trouvé pour",
|
||||
"HEADER": "Header",
|
||||
"BODY": "Body",
|
||||
"FOOTER": "Footer",
|
||||
"BUTTONS": "Buttons",
|
||||
"CATEGORY": "Catégorie",
|
||||
"MEDIA_CONTENT": "Media Content",
|
||||
"MEDIA_CONTENT_FALLBACK": "media content",
|
||||
"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": {
|
||||
"LANGUAGE": "Langue",
|
||||
"TEMPLATE_BODY": "Corps du modèle",
|
||||
"CATEGORY": "Catégorie"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "Variables",
|
||||
"LANGUAGE": "Langue",
|
||||
"CATEGORY": "Catégorie",
|
||||
"VARIABLE_PLACEHOLDER": "Entrez la valeur de {variable}",
|
||||
"GO_BACK_LABEL": "Retour",
|
||||
"SEND_MESSAGE_LABEL": "Envoyer un message",
|
||||
"FORM_ERROR_MESSAGE": "Veuillez remplir toutes les variables avant d'envoyer",
|
||||
"MEDIA_HEADER_LABEL": "{type} Header",
|
||||
"OTP_CODE": "Enter 4-8 digit OTP",
|
||||
"EXPIRY_MINUTES": "Enter expiry minutes",
|
||||
"BUTTON_PARAMETERS": "Button Parameters",
|
||||
"BUTTON_LABEL": "Button {index}",
|
||||
"COUPON_CODE": "Enter coupon code (max 15 chars)",
|
||||
"MEDIA_URL_LABEL": "Enter {type} URL",
|
||||
"BUTTON_PARAMETER": "Enter button parameter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
"CONVERSATION_CREATED": "השיחה נוצרה",
|
||||
"CONVERSATION_UPDATED": "השיחה עודכנה",
|
||||
"MESSAGE_CREATED": "Message Created",
|
||||
"CONVERSATION_RESOLVED": "Conversation Resolved",
|
||||
"CONVERSATION_OPENED": "השיחה נפתחה"
|
||||
},
|
||||
"ACTIONS": {
|
||||
|
||||
@@ -472,7 +472,8 @@
|
||||
"FEATURES": {
|
||||
"TITLE": "מאפיינים",
|
||||
"ALLOW_CONVERSATION_FAQS": "Generate FAQs from resolved conversations",
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions."
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions.",
|
||||
"ALLOW_CITATIONS": "Include source citations in responses"
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
|
||||
@@ -1,29 +1,46 @@
|
||||
{
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "תבניות וואטסאפ",
|
||||
"SUBTITLE": "בחר את תבנית הווטסאפ שברצונך לשלוח",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "עיבוד {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "חפש תבניות",
|
||||
"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": {
|
||||
"LANGUAGE": "שפה",
|
||||
"TEMPLATE_BODY": "גוף התבנית",
|
||||
"CATEGORY": "קטגוריה"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "משתנים",
|
||||
"VARIABLE_PLACEHOLDER": "הזן ערך {variable}",
|
||||
"GO_BACK_LABEL": "חזור",
|
||||
"SEND_MESSAGE_LABEL": "לשלוח הודעה",
|
||||
"FORM_ERROR_MESSAGE": "נא למלא את כל המשתנים לפני השליחה"
|
||||
}
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "תבניות וואטסאפ",
|
||||
"SUBTITLE": "בחר את תבנית הווטסאפ שברצונך לשלוח",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Configure template: {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "חפש תבניות",
|
||||
"NO_TEMPLATES_FOUND": "לא נמצאו תבניות עבור",
|
||||
"HEADER": "Header",
|
||||
"BODY": "Body",
|
||||
"FOOTER": "Footer",
|
||||
"BUTTONS": "Buttons",
|
||||
"CATEGORY": "קטגוריה",
|
||||
"MEDIA_CONTENT": "Media Content",
|
||||
"MEDIA_CONTENT_FALLBACK": "media content",
|
||||
"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": {
|
||||
"LANGUAGE": "שפה",
|
||||
"TEMPLATE_BODY": "גוף התבנית",
|
||||
"CATEGORY": "קטגוריה"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "משתנים",
|
||||
"LANGUAGE": "שפה",
|
||||
"CATEGORY": "קטגוריה",
|
||||
"VARIABLE_PLACEHOLDER": "הזן ערך {variable}",
|
||||
"GO_BACK_LABEL": "חזור",
|
||||
"SEND_MESSAGE_LABEL": "לשלוח הודעה",
|
||||
"FORM_ERROR_MESSAGE": "נא למלא את כל המשתנים לפני השליחה",
|
||||
"MEDIA_HEADER_LABEL": "{type} Header",
|
||||
"OTP_CODE": "Enter 4-8 digit OTP",
|
||||
"EXPIRY_MINUTES": "Enter expiry minutes",
|
||||
"BUTTON_PARAMETERS": "Button Parameters",
|
||||
"BUTTON_LABEL": "Button {index}",
|
||||
"COUPON_CODE": "Enter coupon code (max 15 chars)",
|
||||
"MEDIA_URL_LABEL": "Enter {type} URL",
|
||||
"BUTTON_PARAMETER": "Enter button parameter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
"CONVERSATION_CREATED": "Conversation Created",
|
||||
"CONVERSATION_UPDATED": "Conversation Updated",
|
||||
"MESSAGE_CREATED": "Message Created",
|
||||
"CONVERSATION_RESOLVED": "Conversation Resolved",
|
||||
"CONVERSATION_OPENED": "Conversation Opened"
|
||||
},
|
||||
"ACTIONS": {
|
||||
|
||||
@@ -472,7 +472,8 @@
|
||||
"FEATURES": {
|
||||
"TITLE": "Features",
|
||||
"ALLOW_CONVERSATION_FAQS": "Generate FAQs from resolved conversations",
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions."
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions.",
|
||||
"ALLOW_CITATIONS": "Include source citations in responses"
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
|
||||
@@ -1,29 +1,46 @@
|
||||
{
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "Whatsapp Templates",
|
||||
"SUBTITLE": "Select the whatsapp template you want to send",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Process {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "Search Templates",
|
||||
"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": {
|
||||
"LANGUAGE": "Language",
|
||||
"TEMPLATE_BODY": "Template Body",
|
||||
"CATEGORY": "Category"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "Variables",
|
||||
"VARIABLE_PLACEHOLDER": "Enter {variable} value",
|
||||
"GO_BACK_LABEL": "Go Back",
|
||||
"SEND_MESSAGE_LABEL": "Send Message",
|
||||
"FORM_ERROR_MESSAGE": "Please fill all variables before sending"
|
||||
}
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "Whatsapp Templates",
|
||||
"SUBTITLE": "Select the whatsapp template you want to send",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Configure template: {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "Search Templates",
|
||||
"NO_TEMPLATES_FOUND": "No templates found for",
|
||||
"HEADER": "Header",
|
||||
"BODY": "Body",
|
||||
"FOOTER": "Footer",
|
||||
"BUTTONS": "Buttons",
|
||||
"CATEGORY": "Category",
|
||||
"MEDIA_CONTENT": "Media Content",
|
||||
"MEDIA_CONTENT_FALLBACK": "media content",
|
||||
"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": {
|
||||
"LANGUAGE": "Language",
|
||||
"TEMPLATE_BODY": "Template Body",
|
||||
"CATEGORY": "Category"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "Variables",
|
||||
"LANGUAGE": "Language",
|
||||
"CATEGORY": "Category",
|
||||
"VARIABLE_PLACEHOLDER": "Enter {variable} value",
|
||||
"GO_BACK_LABEL": "Go Back",
|
||||
"SEND_MESSAGE_LABEL": "Send Message",
|
||||
"FORM_ERROR_MESSAGE": "Please fill all variables before sending",
|
||||
"MEDIA_HEADER_LABEL": "{type} Header",
|
||||
"OTP_CODE": "Enter 4-8 digit OTP",
|
||||
"EXPIRY_MINUTES": "Enter expiry minutes",
|
||||
"BUTTON_PARAMETERS": "Button Parameters",
|
||||
"BUTTON_LABEL": "Button {index}",
|
||||
"COUPON_CODE": "Enter coupon code (max 15 chars)",
|
||||
"MEDIA_URL_LABEL": "Enter {type} URL",
|
||||
"BUTTON_PARAMETER": "Enter button parameter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
"CONVERSATION_CREATED": "Conversation Created",
|
||||
"CONVERSATION_UPDATED": "Conversation Updated",
|
||||
"MESSAGE_CREATED": "Message Created",
|
||||
"CONVERSATION_RESOLVED": "Conversation Resolved",
|
||||
"CONVERSATION_OPENED": "Conversation Opened"
|
||||
},
|
||||
"ACTIONS": {
|
||||
|
||||
@@ -472,7 +472,8 @@
|
||||
"FEATURES": {
|
||||
"TITLE": "Features",
|
||||
"ALLOW_CONVERSATION_FAQS": "Generate FAQs from resolved conversations",
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions."
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions.",
|
||||
"ALLOW_CITATIONS": "Include source citations in responses"
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
|
||||
@@ -1,29 +1,46 @@
|
||||
{
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "Whatsapp Predlošci",
|
||||
"SUBTITLE": "Izaberi whatsapp predložak koji želiš poslati",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Proces {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "Pretraži Predloške",
|
||||
"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": {
|
||||
"LANGUAGE": "Jezik",
|
||||
"TEMPLATE_BODY": "Tijelo predloška",
|
||||
"CATEGORY": "Kategorija"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "Varijable",
|
||||
"VARIABLE_PLACEHOLDER": "Unesi {variable} vrijednost",
|
||||
"GO_BACK_LABEL": "Nazad",
|
||||
"SEND_MESSAGE_LABEL": "Šalji poruku",
|
||||
"FORM_ERROR_MESSAGE": "Popuniti sve varijable prije slanja"
|
||||
}
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "Whatsapp Predlošci",
|
||||
"SUBTITLE": "Izaberi whatsapp predložak koji želiš poslati",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Configure template: {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "Pretraži Predloške",
|
||||
"NO_TEMPLATES_FOUND": "Nije pronađen predložak za",
|
||||
"HEADER": "Header",
|
||||
"BODY": "Body",
|
||||
"FOOTER": "Footer",
|
||||
"BUTTONS": "Buttons",
|
||||
"CATEGORY": "Kategorija",
|
||||
"MEDIA_CONTENT": "Media Content",
|
||||
"MEDIA_CONTENT_FALLBACK": "media content",
|
||||
"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": {
|
||||
"LANGUAGE": "Jezik",
|
||||
"TEMPLATE_BODY": "Tijelo predloška",
|
||||
"CATEGORY": "Kategorija"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "Varijable",
|
||||
"LANGUAGE": "Jezik",
|
||||
"CATEGORY": "Kategorija",
|
||||
"VARIABLE_PLACEHOLDER": "Unesi {variable} vrijednost",
|
||||
"GO_BACK_LABEL": "Nazad",
|
||||
"SEND_MESSAGE_LABEL": "Šalji poruku",
|
||||
"FORM_ERROR_MESSAGE": "Popuniti sve varijable prije slanja",
|
||||
"MEDIA_HEADER_LABEL": "{type} Header",
|
||||
"OTP_CODE": "Enter 4-8 digit OTP",
|
||||
"EXPIRY_MINUTES": "Enter expiry minutes",
|
||||
"BUTTON_PARAMETERS": "Button Parameters",
|
||||
"BUTTON_LABEL": "Button {index}",
|
||||
"COUPON_CODE": "Enter coupon code (max 15 chars)",
|
||||
"MEDIA_URL_LABEL": "Enter {type} URL",
|
||||
"BUTTON_PARAMETER": "Enter button parameter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
"CONVERSATION_CREATED": "Beszélgetés létrehozva",
|
||||
"CONVERSATION_UPDATED": "Beszélgetés frissítve",
|
||||
"MESSAGE_CREATED": "Message Created",
|
||||
"CONVERSATION_RESOLVED": "Conversation Resolved",
|
||||
"CONVERSATION_OPENED": "Conversation Opened"
|
||||
},
|
||||
"ACTIONS": {
|
||||
|
||||
@@ -472,7 +472,8 @@
|
||||
"FEATURES": {
|
||||
"TITLE": "Lehetőségek",
|
||||
"ALLOW_CONVERSATION_FAQS": "Generate FAQs from resolved conversations",
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions."
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions.",
|
||||
"ALLOW_CITATIONS": "Include source citations in responses"
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
|
||||
@@ -1,29 +1,46 @@
|
||||
{
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "Whatsapp sablonok",
|
||||
"SUBTITLE": "Válaszd ki a Whatsapp sablont",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Feldolgozás: {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "Sablon keresése",
|
||||
"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": {
|
||||
"LANGUAGE": "Nyelv",
|
||||
"TEMPLATE_BODY": "Sablon törzse",
|
||||
"CATEGORY": "Kategória"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "Változók",
|
||||
"VARIABLE_PLACEHOLDER": "Add meg a {variable} értékét",
|
||||
"GO_BACK_LABEL": "Vissza",
|
||||
"SEND_MESSAGE_LABEL": "Üzenet küldése",
|
||||
"FORM_ERROR_MESSAGE": "Kérlek add meg az összes változó értékét küldés előtt"
|
||||
}
|
||||
"WHATSAPP_TEMPLATES": {
|
||||
"MODAL": {
|
||||
"TITLE": "Whatsapp sablonok",
|
||||
"SUBTITLE": "Válaszd ki a Whatsapp sablont",
|
||||
"TEMPLATE_SELECTED_SUBTITLE": "Configure template: {templateName}"
|
||||
},
|
||||
"PICKER": {
|
||||
"SEARCH_PLACEHOLDER": "Sablon keresése",
|
||||
"NO_TEMPLATES_FOUND": "Nem található sablon erre:",
|
||||
"HEADER": "Header",
|
||||
"BODY": "Body",
|
||||
"FOOTER": "Footer",
|
||||
"BUTTONS": "Buttons",
|
||||
"CATEGORY": "Kategória",
|
||||
"MEDIA_CONTENT": "Media Content",
|
||||
"MEDIA_CONTENT_FALLBACK": "media content",
|
||||
"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": {
|
||||
"LANGUAGE": "Nyelv",
|
||||
"TEMPLATE_BODY": "Sablon törzse",
|
||||
"CATEGORY": "Kategória"
|
||||
}
|
||||
},
|
||||
"PARSER": {
|
||||
"VARIABLES_LABEL": "Változók",
|
||||
"LANGUAGE": "Nyelv",
|
||||
"CATEGORY": "Kategória",
|
||||
"VARIABLE_PLACEHOLDER": "Add meg a {variable} értékét",
|
||||
"GO_BACK_LABEL": "Vissza",
|
||||
"SEND_MESSAGE_LABEL": "Üzenet küldése",
|
||||
"FORM_ERROR_MESSAGE": "Kérlek add meg az összes változó értékét küldés előtt",
|
||||
"MEDIA_HEADER_LABEL": "{type} Header",
|
||||
"OTP_CODE": "Enter 4-8 digit OTP",
|
||||
"EXPIRY_MINUTES": "Enter expiry minutes",
|
||||
"BUTTON_PARAMETERS": "Button Parameters",
|
||||
"BUTTON_LABEL": "Button {index}",
|
||||
"COUPON_CODE": "Enter coupon code (max 15 chars)",
|
||||
"MEDIA_URL_LABEL": "Enter {type} URL",
|
||||
"BUTTON_PARAMETER": "Enter button parameter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
"CONVERSATION_CREATED": "Conversation Created",
|
||||
"CONVERSATION_UPDATED": "Conversation Updated",
|
||||
"MESSAGE_CREATED": "Message Created",
|
||||
"CONVERSATION_RESOLVED": "Conversation Resolved",
|
||||
"CONVERSATION_OPENED": "Conversation Opened"
|
||||
},
|
||||
"ACTIONS": {
|
||||
|
||||
@@ -472,7 +472,8 @@
|
||||
"FEATURES": {
|
||||
"TITLE": "Features",
|
||||
"ALLOW_CONVERSATION_FAQS": "Generate FAQs from resolved conversations",
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions."
|
||||
"ALLOW_MEMORIES": "Capture key details as memories from customer interactions.",
|
||||
"ALLOW_CITATIONS": "Include source citations in responses"
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user